Initial Commit
This commit is contained in:
28
dependencies/amphp/http-client/src/Connection/Connection.php
vendored
Normal file
28
dependencies/amphp/http-client/src/Connection/Connection.php
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
interface Connection
|
||||
{
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise<Stream|null> Returns a stream for the given request, or null if no stream is available or if
|
||||
* the connection is not suited for the given request. The first request for a stream
|
||||
* on a new connection MUST resolve the promise with a Stream instance.
|
||||
*/
|
||||
public function getStream(Request $request) : Promise;
|
||||
/**
|
||||
* @return string[] Array of supported protocol versions.
|
||||
*/
|
||||
public function getProtocolVersions() : array;
|
||||
public function close() : Promise;
|
||||
public function onClose(callable $onClose) : void;
|
||||
public function getLocalAddress() : SocketAddress;
|
||||
public function getRemoteAddress() : SocketAddress;
|
||||
public function getTlsInfo() : ?TlsInfo;
|
||||
}
|
26
dependencies/amphp/http-client/src/Connection/ConnectionFactory.php
vendored
Normal file
26
dependencies/amphp/http-client/src/Connection/ConnectionFactory.php
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
interface ConnectionFactory
|
||||
{
|
||||
/**
|
||||
* During connection establishment, the factory must call the {@see EventListener::startConnectionCreation()},
|
||||
* {@see EventListener::startTlsNegotiation()}, {@see EventListener::completeTlsNegotiation()}, and
|
||||
* {@see EventListener::completeConnectionCreation()} on all event listeners registered on the given request in the
|
||||
* order defined by {@see Request::getEventListeners()} as appropriate (TLS events are only invoked if TLS is
|
||||
* used). Before calling the next listener, the promise returned from the previous one must resolve successfully.
|
||||
*
|
||||
* Additionally, the factory may invoke {@see EventListener::startDnsResolution()} and
|
||||
* {@see EventListener::completeDnsResolution()}, but is not required to implement such granular events.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellationToken
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function create(Request $request, CancellationToken $cancellationToken) : Promise;
|
||||
}
|
282
dependencies/amphp/http-client/src/Connection/ConnectionLimitingPool.php
vendored
Normal file
282
dependencies/amphp/http-client/src/Connection/ConnectionLimitingPool.php
vendored
Normal file
@@ -0,0 +1,282 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Coroutine;
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\MultiReasonException;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
use function WP_Ultimo\Dependencies\Amp\coroutine;
|
||||
final class ConnectionLimitingPool implements ConnectionPool
|
||||
{
|
||||
use ForbidSerialization;
|
||||
/**
|
||||
* Create a connection pool that limits the number of connections per authority.
|
||||
*
|
||||
* @param int $connectionLimit Maximum number of connections allowed to a single authority.
|
||||
* @param ConnectionFactory|null $connectionFactory
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public static function byAuthority(int $connectionLimit, ?ConnectionFactory $connectionFactory = null) : self
|
||||
{
|
||||
return new self($connectionLimit, $connectionFactory);
|
||||
}
|
||||
private static function formatUri(Request $request) : string
|
||||
{
|
||||
$uri = $request->getUri();
|
||||
$scheme = $uri->getScheme();
|
||||
$isHttps = $scheme === 'https';
|
||||
$defaultPort = $isHttps ? 443 : 80;
|
||||
$host = $uri->getHost();
|
||||
$port = $uri->getPort() ?? $defaultPort;
|
||||
$authority = $host . ':' . $port;
|
||||
return $scheme . '://' . $authority;
|
||||
}
|
||||
/** @var int */
|
||||
private $connectionLimit;
|
||||
/** @var ConnectionFactory */
|
||||
private $connectionFactory;
|
||||
/** @var array<string, \ArrayObject<int, Promise<Connection>>> */
|
||||
private $connections = [];
|
||||
/** @var Connection[] */
|
||||
private $idleConnections = [];
|
||||
/** @var int[] */
|
||||
private $activeRequestCounts = [];
|
||||
/** @var Deferred[][] */
|
||||
private $waiting = [];
|
||||
/** @var bool[] */
|
||||
private $waitForPriorConnection = [];
|
||||
/** @var int */
|
||||
private $totalConnectionAttempts = 0;
|
||||
/** @var int */
|
||||
private $totalStreamRequests = 0;
|
||||
/** @var int */
|
||||
private $openConnectionCount = 0;
|
||||
private function __construct(int $connectionLimit, ?ConnectionFactory $connectionFactory = null)
|
||||
{
|
||||
if ($connectionLimit < 1) {
|
||||
throw new \Error('The connection limit must be greater than 0');
|
||||
}
|
||||
$this->connectionLimit = $connectionLimit;
|
||||
$this->connectionFactory = $connectionFactory ?? new DefaultConnectionFactory();
|
||||
}
|
||||
public function __clone()
|
||||
{
|
||||
$this->connections = [];
|
||||
$this->totalConnectionAttempts = 0;
|
||||
$this->totalStreamRequests = 0;
|
||||
$this->openConnectionCount = 0;
|
||||
}
|
||||
public function getTotalConnectionAttempts() : int
|
||||
{
|
||||
return $this->totalConnectionAttempts;
|
||||
}
|
||||
public function getTotalStreamRequests() : int
|
||||
{
|
||||
return $this->totalStreamRequests;
|
||||
}
|
||||
public function getOpenConnectionCount() : int
|
||||
{
|
||||
return $this->openConnectionCount;
|
||||
}
|
||||
public function getStream(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation) {
|
||||
$this->totalStreamRequests++;
|
||||
$uri = self::formatUri($request);
|
||||
// Using new Coroutine avoids a bug on PHP < 7.4, see #265
|
||||
/**
|
||||
* @var Stream $stream
|
||||
* @psalm-suppress all
|
||||
*/
|
||||
[$connection, $stream] = (yield new Coroutine($this->getStreamFor($uri, $request, $cancellation)));
|
||||
$connectionId = \spl_object_id($connection);
|
||||
$this->activeRequestCounts[$connectionId] = ($this->activeRequestCounts[$connectionId] ?? 0) + 1;
|
||||
unset($this->idleConnections[$connectionId]);
|
||||
return HttpStream::fromStream($stream, coroutine(function (Request $request, CancellationToken $cancellationToken) use($connection, $stream, $uri) {
|
||||
try {
|
||||
/** @var Response $response */
|
||||
$response = (yield $stream->request($request, $cancellationToken));
|
||||
} catch (\Throwable $e) {
|
||||
$this->onReadyConnection($connection, $uri);
|
||||
throw $e;
|
||||
}
|
||||
// await response being completely received
|
||||
$response->getTrailers()->onResolve(function () use($connection, $uri) : void {
|
||||
$this->onReadyConnection($connection, $uri);
|
||||
});
|
||||
return $response;
|
||||
}), function () use($connection, $uri) : void {
|
||||
$this->onReadyConnection($connection, $uri);
|
||||
});
|
||||
});
|
||||
}
|
||||
private function getStreamFor(string $uri, Request $request, CancellationToken $cancellation) : \Generator
|
||||
{
|
||||
$isHttps = $request->getUri()->getScheme() === 'https';
|
||||
$connections = $this->connections[$uri] ?? new \ArrayObject();
|
||||
do {
|
||||
foreach ($connections as $connectionPromise) {
|
||||
\assert($connectionPromise instanceof Promise);
|
||||
try {
|
||||
if ($isHttps && ($this->waitForPriorConnection[$uri] ?? \true)) {
|
||||
// Wait for first successful connection if using a secure connection (maybe we can use HTTP/2).
|
||||
$connection = (yield $connectionPromise);
|
||||
} else {
|
||||
$connection = (yield Promise\first([$connectionPromise, new Success()]));
|
||||
if ($connection === null) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $exception) {
|
||||
continue;
|
||||
// Ignore cancellations and errors of other requests.
|
||||
}
|
||||
\assert($connection instanceof Connection);
|
||||
$stream = (yield $this->getStreamFromConnection($connection, $request));
|
||||
if ($stream === null) {
|
||||
if (!$this->isAdditionalConnectionAllowed($uri) && $this->isConnectionIdle($connection)) {
|
||||
$connection->close();
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
// No stream available for the given request.
|
||||
}
|
||||
return [$connection, $stream];
|
||||
}
|
||||
$deferred = new Deferred();
|
||||
$deferredId = \spl_object_id($deferred);
|
||||
$this->waiting[$uri][$deferredId] = $deferred;
|
||||
$deferredPromise = $deferred->promise();
|
||||
$deferredPromise->onResolve(function () use($uri, $deferredId) : void {
|
||||
$this->removeWaiting($uri, $deferredId);
|
||||
});
|
||||
if ($this->isAdditionalConnectionAllowed($uri)) {
|
||||
break;
|
||||
}
|
||||
$connection = (yield $deferredPromise);
|
||||
\assert($connection instanceof Connection);
|
||||
$stream = (yield $this->getStreamFromConnection($connection, $request));
|
||||
if ($stream === null) {
|
||||
continue;
|
||||
// Wait for a different connection to become available.
|
||||
}
|
||||
return [$connection, $stream];
|
||||
} while (\true);
|
||||
$this->totalConnectionAttempts++;
|
||||
$connectionPromise = $this->connectionFactory->create($request, $cancellation);
|
||||
$promiseId = \spl_object_id($connectionPromise);
|
||||
$this->connections[$uri] = $this->connections[$uri] ?? new \ArrayObject();
|
||||
$this->connections[$uri][$promiseId] = $connectionPromise;
|
||||
$connectionPromise->onResolve(function (?\Throwable $exception, ?Connection $connection) use(&$deferred, $uri, $promiseId, $isHttps) : void {
|
||||
if ($exception) {
|
||||
$this->dropConnection($uri, null, $promiseId);
|
||||
if ($deferred !== null) {
|
||||
$deferred->fail($exception);
|
||||
// Fail Deferred so Promise\first() below fails.
|
||||
}
|
||||
return;
|
||||
}
|
||||
\assert($connection !== null);
|
||||
$connectionId = \spl_object_id($connection);
|
||||
$this->openConnectionCount++;
|
||||
if ($isHttps) {
|
||||
$this->waitForPriorConnection[$uri] = \in_array('2', $connection->getProtocolVersions(), \true);
|
||||
}
|
||||
$connection->onClose(function () use($uri, $connectionId, $promiseId) : void {
|
||||
$this->openConnectionCount--;
|
||||
$this->dropConnection($uri, $connectionId, $promiseId);
|
||||
});
|
||||
});
|
||||
try {
|
||||
$connection = (yield Promise\first([$connectionPromise, $deferredPromise]));
|
||||
} catch (MultiReasonException $exception) {
|
||||
[$exception] = $exception->getReasons();
|
||||
// The first reason is why the connection failed.
|
||||
throw $exception;
|
||||
}
|
||||
$deferred = null;
|
||||
// Null reference so connection promise handler does not double-resolve the Deferred.
|
||||
$this->removeWaiting($uri, $deferredId);
|
||||
// Deferred no longer needed for this request.
|
||||
\assert($connection instanceof Connection);
|
||||
$stream = (yield $this->getStreamFromConnection($connection, $request));
|
||||
if ($stream === null) {
|
||||
// Reused connection did not have an available stream for the given request.
|
||||
$connection = (yield $connectionPromise);
|
||||
// Wait for new connection request instead.
|
||||
$stream = (yield $this->getStreamFromConnection($connection, $request));
|
||||
if ($stream === null) {
|
||||
// Other requests used the new connection first, so we need to go around again.
|
||||
// Using new Coroutine avoids a bug on PHP < 7.4, see #265
|
||||
return (yield new Coroutine($this->getStreamFor($uri, $request, $cancellation)));
|
||||
}
|
||||
}
|
||||
return [$connection, $stream];
|
||||
}
|
||||
private function getStreamFromConnection(Connection $connection, Request $request) : Promise
|
||||
{
|
||||
if (!\array_intersect($request->getProtocolVersions(), $connection->getProtocolVersions())) {
|
||||
return new Success();
|
||||
// Connection does not support any of the requested protocol versions.
|
||||
}
|
||||
return $connection->getStream($request);
|
||||
}
|
||||
private function isAdditionalConnectionAllowed(string $uri) : bool
|
||||
{
|
||||
return \count($this->connections[$uri] ?? []) < $this->connectionLimit;
|
||||
}
|
||||
private function onReadyConnection(Connection $connection, string $uri) : void
|
||||
{
|
||||
$connectionId = \spl_object_id($connection);
|
||||
if (isset($this->activeRequestCounts[$connectionId])) {
|
||||
$this->activeRequestCounts[$connectionId]--;
|
||||
if ($this->activeRequestCounts[$connectionId] === 0) {
|
||||
while (\count($this->idleConnections) > 64) {
|
||||
// not customizable for now
|
||||
$idleConnection = \reset($this->idleConnections);
|
||||
$key = \key($this->idleConnections);
|
||||
unset($this->idleConnections[$key]);
|
||||
$idleConnection->close();
|
||||
}
|
||||
$this->idleConnections[$connectionId] = $connection;
|
||||
}
|
||||
}
|
||||
if (empty($this->waiting[$uri])) {
|
||||
return;
|
||||
}
|
||||
$deferred = \reset($this->waiting[$uri]);
|
||||
// Deferred is removed from waiting list in onResolve callback attached above.
|
||||
$deferred->resolve($connection);
|
||||
}
|
||||
private function isConnectionIdle(Connection $connection) : bool
|
||||
{
|
||||
$connectionId = \spl_object_id($connection);
|
||||
\assert(!isset($this->activeRequestCounts[$connectionId]) || $this->activeRequestCounts[$connectionId] >= 0);
|
||||
return ($this->activeRequestCounts[$connectionId] ?? 0) === 0;
|
||||
}
|
||||
private function removeWaiting(string $uri, int $deferredId) : void
|
||||
{
|
||||
unset($this->waiting[$uri][$deferredId]);
|
||||
if (empty($this->waiting[$uri])) {
|
||||
unset($this->waiting[$uri]);
|
||||
}
|
||||
}
|
||||
private function dropConnection(string $uri, ?int $connectionId, int $promiseId) : void
|
||||
{
|
||||
unset($this->connections[$uri][$promiseId]);
|
||||
if ($connectionId !== null) {
|
||||
unset($this->activeRequestCounts[$connectionId], $this->idleConnections[$connectionId]);
|
||||
}
|
||||
if ($this->connections[$uri]->count() === 0) {
|
||||
unset($this->connections[$uri], $this->waitForPriorConnection[$uri]);
|
||||
}
|
||||
}
|
||||
}
|
19
dependencies/amphp/http-client/src/Connection/ConnectionPool.php
vendored
Normal file
19
dependencies/amphp/http-client/src/Connection/ConnectionPool.php
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
interface ConnectionPool
|
||||
{
|
||||
/**
|
||||
* Reserve a stream for a particular request.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
*
|
||||
* @return Promise<Stream>
|
||||
*/
|
||||
public function getStream(Request $request, CancellationToken $cancellation) : Promise;
|
||||
}
|
149
dependencies/amphp/http-client/src/Connection/DefaultConnectionFactory.php
vendored
Normal file
149
dependencies/amphp/http-client/src/Connection/DefaultConnectionFactory.php
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\CancelledException;
|
||||
use WP_Ultimo\Dependencies\Amp\CombinedCancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\InvalidRequestException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\SocketException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\TimeoutException;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\ClientTlsContext;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\ConnectContext;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\Connector;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\EncryptableSocket;
|
||||
use WP_Ultimo\Dependencies\Amp\TimeoutCancellationToken;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
use function WP_Ultimo\Dependencies\Amp\Socket\connector;
|
||||
final class DefaultConnectionFactory implements ConnectionFactory
|
||||
{
|
||||
/** @var Connector|null */
|
||||
private $connector;
|
||||
/** @var ConnectContext|null */
|
||||
private $connectContext;
|
||||
public function __construct(?Connector $connector = null, ?ConnectContext $connectContext = null)
|
||||
{
|
||||
$this->connector = $connector;
|
||||
$this->connectContext = $connectContext;
|
||||
}
|
||||
public function create(Request $request, CancellationToken $cancellationToken) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellationToken) {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startConnectionCreation($request));
|
||||
}
|
||||
$connector = $this->connector ?? connector();
|
||||
$connectContext = $this->connectContext ?? new ConnectContext();
|
||||
$uri = $request->getUri();
|
||||
$scheme = $uri->getScheme();
|
||||
if (!\in_array($scheme, ['http', 'https'], \true)) {
|
||||
throw new InvalidRequestException($request, 'Invalid scheme provided in the request URI: ' . $uri);
|
||||
}
|
||||
$isHttps = $scheme === 'https';
|
||||
$defaultPort = $isHttps ? 443 : 80;
|
||||
$host = $uri->getHost();
|
||||
$port = $uri->getPort() ?? $defaultPort;
|
||||
if ($host === '') {
|
||||
throw new InvalidRequestException($request, 'A host must be provided in the request URI: ' . $uri);
|
||||
}
|
||||
$authority = $host . ':' . $port;
|
||||
$protocolVersions = $request->getProtocolVersions();
|
||||
$isConnect = $request->getMethod() === 'CONNECT';
|
||||
if ($isHttps) {
|
||||
$protocols = [];
|
||||
if (!$isConnect && \in_array('2', $protocolVersions, \true)) {
|
||||
$protocols[] = 'h2';
|
||||
}
|
||||
if (\in_array('1.1', $protocolVersions, \true) || \in_array('1.0', $protocolVersions, \true)) {
|
||||
$protocols[] = 'http/1.1';
|
||||
}
|
||||
if (!$protocols) {
|
||||
throw new InvalidRequestException($request, \sprintf("None of the requested protocol versions (%s) are supported by %s (HTTP/2 is only supported on HTTPS)", \implode(', ', $protocolVersions), self::class));
|
||||
}
|
||||
$tlsContext = ($connectContext->getTlsContext() ?? new ClientTlsContext(''))->withPeerCapturing();
|
||||
// If we only have HTTP/1.1 available, don't set application layer protocols.
|
||||
// There are misbehaving sites like n11.com, see https://github.com/amphp/http-client/issues/255
|
||||
if ($protocols !== ['http/1.1'] && Socket\hasTlsAlpnSupport()) {
|
||||
$tlsContext = $tlsContext->withApplicationLayerProtocols($protocols);
|
||||
}
|
||||
if ($tlsContext->getPeerName() === '') {
|
||||
$tlsContext = $tlsContext->withPeerName($host);
|
||||
}
|
||||
$connectContext = $connectContext->withTlsContext($tlsContext);
|
||||
}
|
||||
try {
|
||||
/** @var EncryptableSocket $socket */
|
||||
$socket = (yield $connector->connect('tcp://' . $authority, $connectContext->withConnectTimeout($request->getTcpConnectTimeout()), $cancellationToken));
|
||||
} catch (Socket\ConnectException $e) {
|
||||
throw new UnprocessedRequestException(new SocketException(\sprintf("Connection to '%s' failed", $authority), 0, $e));
|
||||
} catch (CancelledException $e) {
|
||||
// In case of a user cancellation request, throw the expected exception
|
||||
$cancellationToken->throwIfRequested();
|
||||
// Otherwise we ran into a timeout of our TimeoutCancellationToken
|
||||
throw new UnprocessedRequestException(new TimeoutException(\sprintf("Connection to '%s' timed out, took longer than " . $request->getTcpConnectTimeout() . ' ms', $authority)));
|
||||
// don't pass $e
|
||||
}
|
||||
if ($isHttps) {
|
||||
try {
|
||||
$tlsState = $socket->getTlsState();
|
||||
// Error if anything enabled TLS on a new connection before we can do it
|
||||
if ($tlsState !== EncryptableSocket::TLS_STATE_DISABLED) {
|
||||
$socket->close();
|
||||
throw new UnprocessedRequestException(new SocketException('Failed to setup TLS connection, connection was in an unexpected TLS state (' . $tlsState . ')'));
|
||||
}
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startTlsNegotiation($request));
|
||||
}
|
||||
$tlsCancellationToken = new CombinedCancellationToken($cancellationToken, new TimeoutCancellationToken($request->getTlsHandshakeTimeout()));
|
||||
(yield $socket->setupTls($tlsCancellationToken));
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeTlsNegotiation($request));
|
||||
}
|
||||
} catch (StreamException $exception) {
|
||||
$socket->close();
|
||||
throw new UnprocessedRequestException(new SocketException(\sprintf("Connection to '%s' @ '%s' closed during TLS handshake", $authority, $socket->getRemoteAddress()->toString()), 0, $exception));
|
||||
} catch (CancelledException $e) {
|
||||
$socket->close();
|
||||
// In case of a user cancellation request, throw the expected exception
|
||||
$cancellationToken->throwIfRequested();
|
||||
// Otherwise we ran into a timeout of our TimeoutCancellationToken
|
||||
throw new UnprocessedRequestException(new TimeoutException(\sprintf("TLS handshake with '%s' @ '%s' timed out, took longer than " . $request->getTlsHandshakeTimeout() . ' ms', $authority, $socket->getRemoteAddress()->toString())));
|
||||
// don't pass $e
|
||||
}
|
||||
$tlsInfo = $socket->getTlsInfo();
|
||||
if ($tlsInfo === null) {
|
||||
throw new UnprocessedRequestException(new SocketException(\sprintf("Socket closed after TLS handshake with '%s' @ '%s'", $authority, $socket->getRemoteAddress()->toString())));
|
||||
}
|
||||
if ($tlsInfo->getApplicationLayerProtocol() === 'h2') {
|
||||
$http2Connection = new Http2Connection($socket);
|
||||
(yield $http2Connection->initialize());
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeConnectionCreation($request));
|
||||
}
|
||||
return $http2Connection;
|
||||
}
|
||||
}
|
||||
// Treat the presence of only HTTP/2 as prior knowledge, see https://http2.github.io/http2-spec/#known-http
|
||||
if ($request->getProtocolVersions() === ['2']) {
|
||||
$http2Connection = new Http2Connection($socket);
|
||||
(yield $http2Connection->initialize());
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeConnectionCreation($request));
|
||||
}
|
||||
return $http2Connection;
|
||||
}
|
||||
if (!\array_intersect($request->getProtocolVersions(), ['1.0', '1.1'])) {
|
||||
$socket->close();
|
||||
throw new InvalidRequestException($request, \sprintf("None of the requested protocol versions (%s) are supported by '%s' @ '%s'", \implode(', ', $protocolVersions), $authority, $socket->getRemoteAddress()->toString()));
|
||||
}
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeConnectionCreation($request));
|
||||
}
|
||||
return new Http1Connection($socket);
|
||||
});
|
||||
}
|
||||
}
|
546
dependencies/amphp/http-client/src/Connection/Http1Connection.php
vendored
Normal file
546
dependencies/amphp/http-client/src/Connection/Http1Connection.php
vendored
Normal file
@@ -0,0 +1,546 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\IteratorStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationTokenSource;
|
||||
use WP_Ultimo\Dependencies\Amp\CancelledException;
|
||||
use WP_Ultimo\Dependencies\Amp\CombinedCancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Emitter;
|
||||
use WP_Ultimo\Dependencies\Amp\Http;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Internal\Http1Parser;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Internal\RequestNormalizer;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\HttpException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ResponseBodyStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\InvalidRequestException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ParseException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\SocketException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\TimeoutException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\InvalidHeaderException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Rfc7230;
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\EncryptableSocket;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
use WP_Ultimo\Dependencies\Amp\TimeoutCancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\TimeoutException as PromiseTimeoutException;
|
||||
use function WP_Ultimo\Dependencies\Amp\asyncCall;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
use function WP_Ultimo\Dependencies\Amp\getCurrentTime;
|
||||
use function WP_Ultimo\Dependencies\Amp\Http\Client\Internal\normalizeRequestPathWithQuery;
|
||||
/**
|
||||
* Socket client implementation.
|
||||
*
|
||||
* @see Client
|
||||
*/
|
||||
final class Http1Connection implements Connection
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
private const MAX_KEEP_ALIVE_TIMEOUT = 60;
|
||||
private const PROTOCOL_VERSIONS = ['1.0', '1.1'];
|
||||
/** @var EncryptableSocket|null */
|
||||
private $socket;
|
||||
/** @var bool */
|
||||
private $busy = \false;
|
||||
/** @var int Number of requests made on this connection. */
|
||||
private $requestCounter = 0;
|
||||
/** @var string|null Keep alive timeout watcher ID. */
|
||||
private $timeoutWatcher;
|
||||
/** @var int Keep-Alive timeout from last response. */
|
||||
private $priorTimeout = self::MAX_KEEP_ALIVE_TIMEOUT;
|
||||
/** @var callable[]|null */
|
||||
private $onClose = [];
|
||||
/** @var int */
|
||||
private $timeoutGracePeriod;
|
||||
/** @var int */
|
||||
private $lastUsedAt;
|
||||
/** @var bool */
|
||||
private $explicitTimeout = \false;
|
||||
/** @var SocketAddress */
|
||||
private $localAddress;
|
||||
/** @var SocketAddress */
|
||||
private $remoteAddress;
|
||||
/** @var TlsInfo|null */
|
||||
private $tlsInfo;
|
||||
/** @var Promise|null */
|
||||
private $idleRead;
|
||||
public function __construct(EncryptableSocket $socket, int $timeoutGracePeriod = 2000)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->localAddress = $socket->getLocalAddress();
|
||||
$this->remoteAddress = $socket->getRemoteAddress();
|
||||
$this->tlsInfo = $socket->getTlsInfo();
|
||||
$this->timeoutGracePeriod = $timeoutGracePeriod;
|
||||
$this->lastUsedAt = getCurrentTime();
|
||||
$this->watchIdleConnection();
|
||||
}
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
public function onClose(callable $onClose) : void
|
||||
{
|
||||
if (!$this->socket || $this->socket->isClosed()) {
|
||||
Promise\rethrow(call($onClose, $this));
|
||||
return;
|
||||
}
|
||||
$this->onClose[] = $onClose;
|
||||
}
|
||||
public function close() : Promise
|
||||
{
|
||||
if ($this->socket) {
|
||||
$this->socket->close();
|
||||
}
|
||||
return $this->free();
|
||||
}
|
||||
public function getLocalAddress() : SocketAddress
|
||||
{
|
||||
return $this->localAddress;
|
||||
}
|
||||
public function getRemoteAddress() : SocketAddress
|
||||
{
|
||||
return $this->remoteAddress;
|
||||
}
|
||||
public function getTlsInfo() : ?TlsInfo
|
||||
{
|
||||
return $this->tlsInfo;
|
||||
}
|
||||
public function getProtocolVersions() : array
|
||||
{
|
||||
return self::PROTOCOL_VERSIONS;
|
||||
}
|
||||
public function getStream(Request $request) : Promise
|
||||
{
|
||||
if ($this->busy || $this->requestCounter && !$this->hasStreamFor($request)) {
|
||||
return new Success();
|
||||
}
|
||||
$this->busy = \true;
|
||||
return new Success(HttpStream::fromConnection($this, \Closure::fromCallable([$this, 'request']), \Closure::fromCallable([$this, 'release'])));
|
||||
}
|
||||
private function free() : Promise
|
||||
{
|
||||
$this->socket = null;
|
||||
$this->idleRead = null;
|
||||
$this->lastUsedAt = 0;
|
||||
if ($this->timeoutWatcher !== null) {
|
||||
Loop::cancel($this->timeoutWatcher);
|
||||
}
|
||||
if ($this->onClose !== null) {
|
||||
$onClose = $this->onClose;
|
||||
$this->onClose = null;
|
||||
foreach ($onClose as $callback) {
|
||||
asyncCall($callback, $this);
|
||||
}
|
||||
}
|
||||
return new Success();
|
||||
}
|
||||
private function hasStreamFor(Request $request) : bool
|
||||
{
|
||||
return !$this->busy && $this->socket && !$this->socket->isClosed() && ($this->getRemainingTime() > 0 || $request->isIdempotent());
|
||||
}
|
||||
/** @inheritdoc */
|
||||
private function request(Request $request, CancellationToken $cancellation, Stream $stream) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $stream) {
|
||||
++$this->requestCounter;
|
||||
if ($this->socket !== null && !$this->socket->isClosed()) {
|
||||
$this->socket->reference();
|
||||
}
|
||||
if ($this->timeoutWatcher !== null) {
|
||||
Loop::cancel($this->timeoutWatcher);
|
||||
$this->timeoutWatcher = null;
|
||||
}
|
||||
(yield RequestNormalizer::normalizeRequest($request));
|
||||
$protocolVersion = $this->determineProtocolVersion($request);
|
||||
$request->setProtocolVersions([$protocolVersion]);
|
||||
if ($request->getTransferTimeout() > 0) {
|
||||
$timeoutToken = new TimeoutCancellationToken($request->getTransferTimeout());
|
||||
$combinedCancellation = new CombinedCancellationToken($cancellation, $timeoutToken);
|
||||
} else {
|
||||
$combinedCancellation = $cancellation;
|
||||
}
|
||||
$id = $combinedCancellation->subscribe([$this, 'close']);
|
||||
try {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startSendingRequest($request, $stream));
|
||||
}
|
||||
yield from $this->writeRequest($request, $protocolVersion, $combinedCancellation);
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeSendingRequest($request, $stream));
|
||||
}
|
||||
return yield from $this->readResponse($request, $cancellation, $combinedCancellation, $stream);
|
||||
} catch (\Throwable $e) {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->abort($request, $e));
|
||||
}
|
||||
if ($this->socket !== null) {
|
||||
$this->socket->close();
|
||||
}
|
||||
throw $e;
|
||||
} finally {
|
||||
$combinedCancellation->unsubscribe($id);
|
||||
$cancellation->throwIfRequested();
|
||||
}
|
||||
});
|
||||
}
|
||||
private function release() : void
|
||||
{
|
||||
$this->busy = \false;
|
||||
}
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param CancellationToken $originalCancellation
|
||||
* @param CancellationToken $readingCancellation
|
||||
*
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return \Generator
|
||||
* @throws CancelledException
|
||||
* @throws HttpException
|
||||
* @throws ParseException
|
||||
* @throws SocketException
|
||||
*/
|
||||
private function readResponse(Request $request, CancellationToken $originalCancellation, CancellationToken $readingCancellation, Stream $stream) : \Generator
|
||||
{
|
||||
$bodyEmitter = new Emitter();
|
||||
$backpressure = new Success();
|
||||
$bodyCallback = static function ($data) use($bodyEmitter, &$backpressure) : void {
|
||||
$backpressure = $bodyEmitter->emit($data);
|
||||
};
|
||||
$trailersDeferred = new Deferred();
|
||||
$trailers = [];
|
||||
$trailersCallback = static function (array $headers) use(&$trailers) : void {
|
||||
$trailers = $headers;
|
||||
};
|
||||
$parser = new Http1Parser($request, $bodyCallback, $trailersCallback);
|
||||
$start = getCurrentTime();
|
||||
$timeout = $request->getInactivityTimeout();
|
||||
try {
|
||||
if ($this->socket === null) {
|
||||
throw new SocketException('Socket closed prior to response completion');
|
||||
}
|
||||
while (null !== ($chunk = (yield $timeout > 0 ? Promise\timeout($this->idleRead ?: $this->socket->read(), $timeout) : ($this->idleRead ?: $this->socket->read())))) {
|
||||
$this->idleRead = null;
|
||||
parseChunk:
|
||||
$response = $parser->parse($chunk);
|
||||
if ($response === null) {
|
||||
if ($this->socket === null) {
|
||||
throw new SocketException('Socket closed prior to response completion');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$this->lastUsedAt = getCurrentTime();
|
||||
$status = $response->getStatus();
|
||||
if ($status === Http\Status::SWITCHING_PROTOCOLS) {
|
||||
$connection = Http\createFieldValueComponentMap(Http\parseFieldValueComponents($response, 'connection'));
|
||||
if (!isset($connection['upgrade'])) {
|
||||
throw new HttpException('Switching protocols response missing "Connection: upgrade" header');
|
||||
}
|
||||
if (!$response->hasHeader('upgrade')) {
|
||||
throw new HttpException('Switching protocols response missing "Upgrade" header');
|
||||
}
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeReceivingResponse($request, $stream));
|
||||
}
|
||||
$trailersDeferred->resolve($trailers);
|
||||
return $this->handleUpgradeResponse($request, $response, $parser->getBuffer());
|
||||
}
|
||||
if ($status < 200) {
|
||||
// 1XX responses (excluding 101, handled above)
|
||||
$onInformationalResponse = $request->getInformationalResponseHandler();
|
||||
if ($onInformationalResponse !== null) {
|
||||
(yield call($onInformationalResponse, $response));
|
||||
}
|
||||
$chunk = $parser->getBuffer();
|
||||
$parser = new Http1Parser($request, $bodyCallback, $trailersCallback);
|
||||
goto parseChunk;
|
||||
}
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startReceivingResponse($request, $stream));
|
||||
}
|
||||
if ($status >= 200 && $status < 300 && $request->getMethod() === 'CONNECT') {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeReceivingResponse($request, $stream));
|
||||
}
|
||||
$trailersDeferred->resolve($trailers);
|
||||
return $this->handleUpgradeResponse($request, $response, $parser->getBuffer());
|
||||
}
|
||||
$bodyCancellationSource = new CancellationTokenSource();
|
||||
$bodyCancellationToken = new CombinedCancellationToken($readingCancellation, $bodyCancellationSource->getToken());
|
||||
$response->setTrailers($trailersDeferred->promise());
|
||||
$response->setBody(new ResponseBodyStream(new IteratorStream($bodyEmitter->iterate()), $bodyCancellationSource));
|
||||
// Read body async
|
||||
asyncCall(function () use($parser, $request, $response, $bodyEmitter, $trailersDeferred, $originalCancellation, $readingCancellation, $bodyCancellationToken, $stream, $timeout, &$backpressure, &$trailers) {
|
||||
$id = $bodyCancellationToken->subscribe([$this, 'close']);
|
||||
try {
|
||||
// Required, otherwise responses without body hang
|
||||
if (!$parser->isComplete()) {
|
||||
// Directly parse again in case we already have the full body but aborted parsing
|
||||
// to resolve promise with headers.
|
||||
$chunk = null;
|
||||
try {
|
||||
/** @psalm-suppress PossiblyNullReference */
|
||||
do {
|
||||
/** @noinspection CallableParameterUseCaseInTypeContextInspection */
|
||||
$parser->parse($chunk);
|
||||
/**
|
||||
* @noinspection NotOptimalIfConditionsInspection
|
||||
* @psalm-suppress TypeDoesNotContainType
|
||||
*/
|
||||
if ($parser->isComplete()) {
|
||||
break;
|
||||
}
|
||||
if (!$backpressure instanceof Success) {
|
||||
(yield $this->withCancellation($backpressure, $bodyCancellationToken));
|
||||
}
|
||||
/** @psalm-suppress TypeDoesNotContainNull */
|
||||
if ($this->socket === null) {
|
||||
throw new SocketException('Socket closed prior to response completion');
|
||||
}
|
||||
} while (null !== ($chunk = (yield $timeout > 0 ? Promise\timeout($this->socket->read(), $timeout) : $this->socket->read())));
|
||||
} catch (PromiseTimeoutException $e) {
|
||||
$this->close();
|
||||
throw new TimeoutException('Inactivity timeout exceeded, more than ' . $timeout . ' ms elapsed from last data received', 0, $e);
|
||||
}
|
||||
$originalCancellation->throwIfRequested();
|
||||
if ($readingCancellation->isRequested()) {
|
||||
throw new TimeoutException('Allowed transfer timeout exceeded, took longer than ' . $request->getTransferTimeout() . ' ms');
|
||||
}
|
||||
$bodyCancellationToken->throwIfRequested();
|
||||
// Ignore check if neither content-length nor chunked encoding are given.
|
||||
/** @psalm-suppress RedundantCondition */
|
||||
if (!$parser->isComplete() && $parser->getState() !== Http1Parser::BODY_IDENTITY_EOF) {
|
||||
throw new SocketException('Socket disconnected prior to response completion');
|
||||
}
|
||||
}
|
||||
$timeout = $this->determineKeepAliveTimeout($response);
|
||||
if ($timeout > 0 && $parser->getState() !== Http1Parser::BODY_IDENTITY_EOF) {
|
||||
$this->timeoutWatcher = Loop::delay($timeout * 1000, [$this, 'close']);
|
||||
Loop::unreference($this->timeoutWatcher);
|
||||
$this->watchIdleConnection();
|
||||
} else {
|
||||
$this->close();
|
||||
}
|
||||
$this->busy = \false;
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->completeReceivingResponse($request, $stream));
|
||||
}
|
||||
$bodyEmitter->complete();
|
||||
$trailersDeferred->resolve($trailers);
|
||||
} catch (\Throwable $e) {
|
||||
$this->close();
|
||||
try {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->abort($request, $e));
|
||||
}
|
||||
} finally {
|
||||
$bodyEmitter->fail($e);
|
||||
$trailersDeferred->fail($e);
|
||||
}
|
||||
} finally {
|
||||
$bodyCancellationToken->unsubscribe($id);
|
||||
}
|
||||
});
|
||||
return $response;
|
||||
}
|
||||
$originalCancellation->throwIfRequested();
|
||||
throw new SocketException(\sprintf("Receiving the response headers for '%s' failed, because the socket to '%s' @ '%s' closed early with %d bytes received within %d milliseconds", (string) $request->getUri()->withUserInfo(''), $request->getUri()->withUserInfo('')->getAuthority(), $this->socket === null ? '???' : (string) $this->socket->getRemoteAddress(), \strlen($parser->getBuffer()), getCurrentTime() - $start));
|
||||
} catch (HttpException $e) {
|
||||
$this->close();
|
||||
throw $e;
|
||||
} catch (PromiseTimeoutException $e) {
|
||||
$this->close();
|
||||
throw new TimeoutException('Inactivity timeout exceeded, more than ' . $timeout . ' ms elapsed from last data received', 0, $e);
|
||||
} catch (\Throwable $e) {
|
||||
$this->close();
|
||||
throw new SocketException('Receiving the response headers failed: ' . $e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
private function handleUpgradeResponse(Request $request, Response $response, string $buffer) : Response
|
||||
{
|
||||
if ($this->socket === null) {
|
||||
throw new SocketException('Socket closed while upgrading');
|
||||
}
|
||||
$socket = new UpgradedSocket($this->socket, $buffer);
|
||||
$this->free();
|
||||
// Mark this connection as unusable without closing socket.
|
||||
if (($onUpgrade = $request->getUpgradeHandler()) === null) {
|
||||
$socket->close();
|
||||
throw new HttpException('CONNECT or upgrade request made without upgrade handler callback');
|
||||
}
|
||||
asyncCall(static function () use($onUpgrade, $socket, $request, $response) : \Generator {
|
||||
try {
|
||||
(yield call($onUpgrade, $socket, $request, $response));
|
||||
} catch (\Throwable $exception) {
|
||||
$socket->close();
|
||||
throw new HttpException('Upgrade handler threw an exception', 0, $exception);
|
||||
}
|
||||
});
|
||||
return $response;
|
||||
}
|
||||
/**
|
||||
* @return int Approximate number of milliseconds remaining until the connection is closed.
|
||||
*/
|
||||
private function getRemainingTime() : int
|
||||
{
|
||||
$timestamp = $this->lastUsedAt + ($this->explicitTimeout ? $this->priorTimeout * 1000 : $this->timeoutGracePeriod);
|
||||
return \max(0, $timestamp - getCurrentTime());
|
||||
}
|
||||
private function withCancellation(Promise $promise, CancellationToken $cancellationToken) : Promise
|
||||
{
|
||||
$deferred = new Deferred();
|
||||
$newPromise = $deferred->promise();
|
||||
$promise->onResolve(static function ($error, $value) use(&$deferred) : void {
|
||||
if ($deferred) {
|
||||
$temp = $deferred;
|
||||
$deferred = null;
|
||||
if ($error) {
|
||||
$temp->fail($error);
|
||||
} else {
|
||||
$temp->resolve($value);
|
||||
}
|
||||
}
|
||||
});
|
||||
$cancellationSubscription = $cancellationToken->subscribe(static function ($e) use(&$deferred) : void {
|
||||
if ($deferred) {
|
||||
$temp = $deferred;
|
||||
$deferred = null;
|
||||
$temp->fail($e);
|
||||
}
|
||||
});
|
||||
$newPromise->onResolve(static function () use($cancellationToken, $cancellationSubscription) : void {
|
||||
$cancellationToken->unsubscribe($cancellationSubscription);
|
||||
});
|
||||
return $newPromise;
|
||||
}
|
||||
private function determineKeepAliveTimeout(Response $response) : int
|
||||
{
|
||||
$request = $response->getRequest();
|
||||
$requestConnHeader = $request->getHeader('connection') ?? '';
|
||||
$responseConnHeader = $response->getHeader('connection') ?? '';
|
||||
if (!\strcasecmp($requestConnHeader, 'close')) {
|
||||
return 0;
|
||||
}
|
||||
if ($response->getProtocolVersion() === '1.0') {
|
||||
return 0;
|
||||
}
|
||||
if (!\strcasecmp($responseConnHeader, 'close')) {
|
||||
return 0;
|
||||
}
|
||||
$params = Http\createFieldValueComponentMap(Http\parseFieldValueComponents($response, 'keep-alive'));
|
||||
$timeout = (int) ($params['timeout'] ?? $this->priorTimeout);
|
||||
if (isset($params['timeout'])) {
|
||||
$this->explicitTimeout = \true;
|
||||
}
|
||||
return $this->priorTimeout = \min(\max(0, $timeout), self::MAX_KEEP_ALIVE_TIMEOUT);
|
||||
}
|
||||
private function determineProtocolVersion(Request $request) : string
|
||||
{
|
||||
$protocolVersions = $request->getProtocolVersions();
|
||||
if (\in_array("1.1", $protocolVersions, \true)) {
|
||||
return "1.1";
|
||||
}
|
||||
if (\in_array("1.0", $protocolVersions, \true)) {
|
||||
return "1.0";
|
||||
}
|
||||
throw new InvalidRequestException($request, "None of the requested protocol versions is supported: " . \implode(", ", $protocolVersions));
|
||||
}
|
||||
private function writeRequest(Request $request, string $protocolVersion, CancellationToken $cancellation) : \Generator
|
||||
{
|
||||
try {
|
||||
$rawHeaders = $this->generateRawHeader($request, $protocolVersion);
|
||||
if ($this->socket === null) {
|
||||
throw new UnprocessedRequestException(new SocketException('Socket closed before request started'));
|
||||
}
|
||||
(yield $this->socket->write($rawHeaders));
|
||||
if ($request->getMethod() === 'CONNECT') {
|
||||
return;
|
||||
}
|
||||
$body = $request->getBody()->createBodyStream();
|
||||
$chunking = $request->getHeader("transfer-encoding") === "chunked";
|
||||
$remainingBytes = $request->getHeader("content-length");
|
||||
if ($remainingBytes !== null) {
|
||||
$remainingBytes = (int) $remainingBytes;
|
||||
}
|
||||
if ($chunking && $protocolVersion === "1.0") {
|
||||
throw new InvalidRequestException($request, "Can't send chunked bodies over HTTP/1.0");
|
||||
}
|
||||
// We always buffer the last chunk to make sure we don't write $contentLength bytes if the body is too long.
|
||||
$buffer = "";
|
||||
while (null !== ($chunk = (yield $body->read()))) {
|
||||
$cancellation->throwIfRequested();
|
||||
if ($chunk === "") {
|
||||
continue;
|
||||
}
|
||||
if ($chunking) {
|
||||
$chunk = \dechex(\strlen($chunk)) . "\r\n" . $chunk . "\r\n";
|
||||
} elseif ($remainingBytes !== null) {
|
||||
$remainingBytes -= \strlen($chunk);
|
||||
if ($remainingBytes < 0) {
|
||||
throw new InvalidRequestException($request, "Body contained more bytes than specified in Content-Length, aborting request");
|
||||
}
|
||||
}
|
||||
(yield $this->socket->write($buffer));
|
||||
$buffer = $chunk;
|
||||
}
|
||||
$cancellation->throwIfRequested();
|
||||
// Flush last buffered chunk.
|
||||
(yield $this->socket->write($buffer));
|
||||
if ($chunking) {
|
||||
(yield $this->socket->write("0\r\n\r\n"));
|
||||
} elseif ($remainingBytes !== null && $remainingBytes > 0) {
|
||||
throw new InvalidRequestException($request, "Body contained fewer bytes than specified in Content-Length, aborting request");
|
||||
}
|
||||
} catch (StreamException $exception) {
|
||||
throw new SocketException('Socket disconnected prior to response completion');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $protocolVersion
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @throws HttpException
|
||||
*/
|
||||
private function generateRawHeader(Request $request, string $protocolVersion) : string
|
||||
{
|
||||
$uri = $request->getUri();
|
||||
$requestUri = normalizeRequestPathWithQuery($request);
|
||||
$method = $request->getMethod();
|
||||
if ($method === 'CONNECT') {
|
||||
$defaultPort = $uri->getScheme() === 'https' ? 443 : 80;
|
||||
$requestUri = $uri->getHost() . ':' . ($uri->getPort() ?? $defaultPort);
|
||||
}
|
||||
$header = $method . ' ' . $requestUri . ' HTTP/' . $protocolVersion . "\r\n";
|
||||
try {
|
||||
$header .= Rfc7230::formatRawHeaders($request->getRawHeaders());
|
||||
} catch (InvalidHeaderException $e) {
|
||||
throw new HttpException($e->getMessage());
|
||||
}
|
||||
return $header . "\r\n";
|
||||
}
|
||||
private function watchIdleConnection() : void
|
||||
{
|
||||
if ($this->socket === null || $this->socket->isClosed()) {
|
||||
return;
|
||||
}
|
||||
$this->socket->unreference();
|
||||
$this->idleRead = $this->socket->read();
|
||||
$this->idleRead->onResolve(function ($error, $chunk) {
|
||||
if ($error || $chunk === null) {
|
||||
$this->close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
78
dependencies/amphp/http-client/src/Connection/Http2Connection.php
vendored
Normal file
78
dependencies/amphp/http-client/src/Connection/Http2Connection.php
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Internal\Http2ConnectionProcessor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\EncryptableSocket;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class Http2Connection implements Connection
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
private const PROTOCOL_VERSIONS = ['2'];
|
||||
/** @var EncryptableSocket */
|
||||
private $socket;
|
||||
/** @var Http2ConnectionProcessor */
|
||||
private $processor;
|
||||
/** @var int */
|
||||
private $requestCount = 0;
|
||||
public function __construct(EncryptableSocket $socket)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->processor = new Http2ConnectionProcessor($socket);
|
||||
}
|
||||
public function getProtocolVersions() : array
|
||||
{
|
||||
return self::PROTOCOL_VERSIONS;
|
||||
}
|
||||
public function initialize() : Promise
|
||||
{
|
||||
return $this->processor->initialize();
|
||||
}
|
||||
public function getStream(Request $request) : Promise
|
||||
{
|
||||
if (!$this->processor->isInitialized()) {
|
||||
throw new \Error('The promise returned from ' . __CLASS__ . '::initialize() must resolve before using the connection');
|
||||
}
|
||||
return call(function () {
|
||||
if ($this->processor->isClosed() || $this->processor->getRemainingStreams() <= 0) {
|
||||
return null;
|
||||
}
|
||||
$this->processor->reserveStream();
|
||||
return HttpStream::fromConnection($this, \Closure::fromCallable([$this, 'request']), \Closure::fromCallable([$this->processor, 'unreserveStream']));
|
||||
});
|
||||
}
|
||||
public function onClose(callable $onClose) : void
|
||||
{
|
||||
$this->processor->onClose($onClose);
|
||||
}
|
||||
public function close() : Promise
|
||||
{
|
||||
return $this->processor->close();
|
||||
}
|
||||
public function getLocalAddress() : SocketAddress
|
||||
{
|
||||
return $this->socket->getLocalAddress();
|
||||
}
|
||||
public function getRemoteAddress() : SocketAddress
|
||||
{
|
||||
return $this->socket->getRemoteAddress();
|
||||
}
|
||||
public function getTlsInfo() : ?TlsInfo
|
||||
{
|
||||
return $this->socket->getTlsInfo();
|
||||
}
|
||||
private function request(Request $request, CancellationToken $token, Stream $applicationStream) : Promise
|
||||
{
|
||||
$this->requestCount++;
|
||||
return $this->processor->request($request, $token, $applicationStream);
|
||||
}
|
||||
}
|
15
dependencies/amphp/http-client/src/Connection/Http2ConnectionException.php
vendored
Normal file
15
dependencies/amphp/http-client/src/Connection/Http2ConnectionException.php
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\HttpException;
|
||||
/**
|
||||
* @deprecated Exception moved to amphp/http. Catch the base exception class (HttpException) instead.
|
||||
*/
|
||||
final class Http2ConnectionException extends HttpException
|
||||
{
|
||||
public function __construct(string $message, int $code, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
22
dependencies/amphp/http-client/src/Connection/Http2StreamException.php
vendored
Normal file
22
dependencies/amphp/http-client/src/Connection/Http2StreamException.php
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\HttpException;
|
||||
/**
|
||||
* @deprecated Exception moved to amphp/http. Catch the base exception class (HttpException) instead.
|
||||
*/
|
||||
final class Http2StreamException extends HttpException
|
||||
{
|
||||
/** @var int */
|
||||
private $streamId;
|
||||
public function __construct(string $message, int $streamId, int $code, ?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->streamId = $streamId;
|
||||
}
|
||||
public function getStreamId() : int
|
||||
{
|
||||
return $this->streamId;
|
||||
}
|
||||
}
|
74
dependencies/amphp/http-client/src/Connection/HttpStream.php
vendored
Normal file
74
dependencies/amphp/http-client/src/Connection/HttpStream.php
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class HttpStream implements Stream
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
public static function fromConnection(Connection $connection, callable $requestCallback, callable $releaseCallback) : self
|
||||
{
|
||||
return new self($connection->getLocalAddress(), $connection->getRemoteAddress(), $connection->getTlsInfo(), $requestCallback, $releaseCallback);
|
||||
}
|
||||
public static function fromStream(Stream $stream, callable $requestCallback, callable $releaseCallback) : self
|
||||
{
|
||||
return new self($stream->getLocalAddress(), $stream->getRemoteAddress(), $stream->getTlsInfo(), $requestCallback, $releaseCallback);
|
||||
}
|
||||
/** @var SocketAddress */
|
||||
private $localAddress;
|
||||
/** @var SocketAddress */
|
||||
private $remoteAddress;
|
||||
/** @var TlsInfo|null */
|
||||
private $tlsInfo;
|
||||
/** @var callable */
|
||||
private $requestCallback;
|
||||
/** @var callable|null */
|
||||
private $releaseCallback;
|
||||
private function __construct(SocketAddress $localAddress, SocketAddress $remoteAddress, ?TlsInfo $tlsInfo, callable $requestCallback, callable $releaseCallback)
|
||||
{
|
||||
$this->localAddress = $localAddress;
|
||||
$this->remoteAddress = $remoteAddress;
|
||||
$this->tlsInfo = $tlsInfo;
|
||||
$this->requestCallback = $requestCallback;
|
||||
$this->releaseCallback = $releaseCallback;
|
||||
}
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->releaseCallback !== null) {
|
||||
($this->releaseCallback)();
|
||||
}
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
if ($this->releaseCallback === null) {
|
||||
throw new \Error('A stream may only be used for a single request');
|
||||
}
|
||||
$this->releaseCallback = null;
|
||||
return call(function () use($request, $cancellation) {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startRequest($request));
|
||||
}
|
||||
return call($this->requestCallback, $request, $cancellation, $this);
|
||||
});
|
||||
}
|
||||
public function getLocalAddress() : SocketAddress
|
||||
{
|
||||
return $this->localAddress;
|
||||
}
|
||||
public function getRemoteAddress() : SocketAddress
|
||||
{
|
||||
return $this->remoteAddress;
|
||||
}
|
||||
public function getTlsInfo() : ?TlsInfo
|
||||
{
|
||||
return $this->tlsInfo;
|
||||
}
|
||||
}
|
53
dependencies/amphp/http-client/src/Connection/InterceptedStream.php
vendored
Normal file
53
dependencies/amphp/http-client/src/Connection/InterceptedStream.php
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\NetworkInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class InterceptedStream implements Stream
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var Stream */
|
||||
private $stream;
|
||||
/** @var NetworkInterceptor|null */
|
||||
private $interceptor;
|
||||
public function __construct(Stream $stream, NetworkInterceptor $interceptor)
|
||||
{
|
||||
$this->stream = $stream;
|
||||
$this->interceptor = $interceptor;
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
if (!$this->interceptor) {
|
||||
throw new \Error(__METHOD__ . ' may only be invoked once per instance. ' . 'If you need to implement retries or otherwise issue multiple requests, register an ApplicationInterceptor to do so.');
|
||||
}
|
||||
$interceptor = $this->interceptor;
|
||||
$this->interceptor = null;
|
||||
return call(function () use($interceptor, $request, $cancellation) {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startRequest($request));
|
||||
}
|
||||
return $interceptor->requestViaNetwork($request, $cancellation, $this->stream);
|
||||
});
|
||||
}
|
||||
public function getLocalAddress() : SocketAddress
|
||||
{
|
||||
return $this->stream->getLocalAddress();
|
||||
}
|
||||
public function getRemoteAddress() : SocketAddress
|
||||
{
|
||||
return $this->stream->getRemoteAddress();
|
||||
}
|
||||
public function getTlsInfo() : ?TlsInfo
|
||||
{
|
||||
return $this->stream->getTlsInfo();
|
||||
}
|
||||
}
|
344
dependencies/amphp/http-client/src/Connection/Internal/Http1Parser.php
vendored
Normal file
344
dependencies/amphp/http-client/src/Connection/Internal/Http1Parser.php
vendored
Normal file
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InMemoryStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ParseException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\InvalidHeaderException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Rfc7230;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Status;
|
||||
/** @internal */
|
||||
final class Http1Parser
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
private const STATUS_LINE_PATTERN = "#^\n HTTP/(?P<protocol>\\d+\\.\\d+)[ \t]+\n (?P<status>[1-9]\\d\\d)[ \t]*\n (?P<reason>[^\x01-\x08\x10-\x19]*)\n \$#ix";
|
||||
public const AWAITING_HEADERS = 0;
|
||||
public const BODY_IDENTITY = 1;
|
||||
public const BODY_IDENTITY_EOF = 2;
|
||||
public const BODY_CHUNKS = 3;
|
||||
public const TRAILERS_START = 4;
|
||||
public const TRAILERS = 5;
|
||||
/** @var int */
|
||||
private $state = self::AWAITING_HEADERS;
|
||||
/** @var string */
|
||||
private $buffer = '';
|
||||
/** @var string|null */
|
||||
private $protocol;
|
||||
/** @var int|null */
|
||||
private $statusCode;
|
||||
/** @var string|null */
|
||||
private $statusReason;
|
||||
/** @var string[][] */
|
||||
private $headers = [];
|
||||
/** @var int|null */
|
||||
private $remainingBodyBytes;
|
||||
/** @var int */
|
||||
private $bodyBytesConsumed = 0;
|
||||
/** @var bool */
|
||||
private $chunkedEncoding = \false;
|
||||
/** @var int|null */
|
||||
private $chunkLengthRemaining;
|
||||
/** @var bool */
|
||||
private $complete = \false;
|
||||
/** @var Request */
|
||||
private $request;
|
||||
/** @var int */
|
||||
private $maxHeaderBytes;
|
||||
/** @var int */
|
||||
private $maxBodyBytes;
|
||||
/** @var callable */
|
||||
private $bodyDataCallback;
|
||||
/** @var callable */
|
||||
private $trailersCallback;
|
||||
public function __construct(Request $request, callable $bodyDataCallback, callable $trailersCallback)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->bodyDataCallback = $bodyDataCallback;
|
||||
$this->trailersCallback = $trailersCallback;
|
||||
$this->maxHeaderBytes = $request->getHeaderSizeLimit();
|
||||
$this->maxBodyBytes = $request->getBodySizeLimit();
|
||||
}
|
||||
public function getBuffer() : string
|
||||
{
|
||||
return $this->buffer;
|
||||
}
|
||||
public function getState() : int
|
||||
{
|
||||
return $this->state;
|
||||
}
|
||||
public function buffer(string $data) : void
|
||||
{
|
||||
$this->buffer .= $data;
|
||||
}
|
||||
/**
|
||||
* @param string|null $data
|
||||
*
|
||||
* @return Response|null
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
public function parse(string $data = null) : ?Response
|
||||
{
|
||||
if ($data !== null) {
|
||||
$this->buffer .= $data;
|
||||
}
|
||||
if ($this->buffer === '') {
|
||||
return null;
|
||||
}
|
||||
if ($this->complete) {
|
||||
throw new ParseException('Can\'t continue parsing, response is already complete', Status::BAD_REQUEST);
|
||||
}
|
||||
switch ($this->state) {
|
||||
case self::AWAITING_HEADERS:
|
||||
goto headers;
|
||||
case self::BODY_IDENTITY:
|
||||
goto body_identity;
|
||||
case self::BODY_IDENTITY_EOF:
|
||||
goto body_identity_eof;
|
||||
case self::BODY_CHUNKS:
|
||||
goto body_chunks;
|
||||
case self::TRAILERS_START:
|
||||
goto trailers_start;
|
||||
case self::TRAILERS:
|
||||
goto trailers;
|
||||
}
|
||||
headers:
|
||||
$startLineAndHeaders = $this->shiftHeadersFromBuffer();
|
||||
if ($startLineAndHeaders === null) {
|
||||
return null;
|
||||
}
|
||||
$startLineEndPos = \strpos($startLineAndHeaders, "\r\n");
|
||||
\assert($startLineEndPos !== \false);
|
||||
$startLine = \substr($startLineAndHeaders, 0, $startLineEndPos);
|
||||
$rawHeaders = \substr($startLineAndHeaders, $startLineEndPos + 2);
|
||||
if (\preg_match(self::STATUS_LINE_PATTERN, $startLine, $match)) {
|
||||
$this->protocol = $match['protocol'];
|
||||
$this->statusCode = (int) $match['status'];
|
||||
$this->statusReason = \trim($match['reason']);
|
||||
} else {
|
||||
throw new ParseException('Invalid status line: ' . $startLine, Status::BAD_REQUEST);
|
||||
}
|
||||
if ($rawHeaders !== '') {
|
||||
$this->headers = $this->parseRawHeaders($rawHeaders);
|
||||
} else {
|
||||
$this->headers = [];
|
||||
}
|
||||
$requestMethod = $this->request->getMethod();
|
||||
$skipBody = $this->statusCode < Status::OK || $this->statusCode === Status::NOT_MODIFIED || $this->statusCode === Status::NO_CONTENT || $requestMethod === 'HEAD' || $requestMethod === 'CONNECT';
|
||||
if ($skipBody) {
|
||||
$this->complete = \true;
|
||||
} elseif ($this->chunkedEncoding) {
|
||||
$this->state = self::BODY_CHUNKS;
|
||||
} elseif ($this->remainingBodyBytes === null) {
|
||||
$this->state = self::BODY_IDENTITY_EOF;
|
||||
} elseif ($this->remainingBodyBytes > 0) {
|
||||
$this->state = self::BODY_IDENTITY;
|
||||
} else {
|
||||
$this->complete = \true;
|
||||
}
|
||||
$response = new Response($this->protocol, $this->statusCode, $this->statusReason, [], new InMemoryStream(), $this->request);
|
||||
foreach ($this->headers as [$key, $value]) {
|
||||
$response->addHeader($key, $value);
|
||||
}
|
||||
return $response;
|
||||
body_identity:
|
||||
$bufferDataSize = \strlen($this->buffer);
|
||||
if ($bufferDataSize <= $this->remainingBodyBytes) {
|
||||
$chunk = $this->buffer;
|
||||
$this->buffer = '';
|
||||
$this->remainingBodyBytes -= $bufferDataSize;
|
||||
$this->addToBody($chunk);
|
||||
if ($this->remainingBodyBytes === 0) {
|
||||
$this->complete = \true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
$bodyData = \substr($this->buffer, 0, $this->remainingBodyBytes);
|
||||
$this->addToBody($bodyData);
|
||||
$this->buffer = \substr($this->buffer, $this->remainingBodyBytes);
|
||||
$this->remainingBodyBytes = 0;
|
||||
goto complete;
|
||||
body_identity_eof:
|
||||
$this->addToBody($this->buffer);
|
||||
$this->buffer = '';
|
||||
return null;
|
||||
body_chunks:
|
||||
if ($this->parseChunkedBody()) {
|
||||
$this->state = self::TRAILERS_START;
|
||||
goto trailers_start;
|
||||
}
|
||||
return null;
|
||||
trailers_start:
|
||||
$firstTwoBytes = \substr($this->buffer, 0, 2);
|
||||
if ($firstTwoBytes === "" || $firstTwoBytes === "\r") {
|
||||
return null;
|
||||
}
|
||||
if ($firstTwoBytes === "\r\n") {
|
||||
$this->buffer = \substr($this->buffer, 2);
|
||||
goto complete;
|
||||
}
|
||||
$this->state = self::TRAILERS;
|
||||
goto trailers;
|
||||
trailers:
|
||||
$trailers = $this->shiftHeadersFromBuffer();
|
||||
if ($trailers === null) {
|
||||
return null;
|
||||
}
|
||||
$this->parseTrailers($trailers);
|
||||
goto complete;
|
||||
complete:
|
||||
$this->complete = \true;
|
||||
return null;
|
||||
}
|
||||
public function isComplete() : bool
|
||||
{
|
||||
return $this->complete;
|
||||
}
|
||||
/**
|
||||
* @return string|null
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function shiftHeadersFromBuffer() : ?string
|
||||
{
|
||||
$this->buffer = \ltrim($this->buffer, "\r\n");
|
||||
if ($headersSize = \strpos($this->buffer, "\r\n\r\n")) {
|
||||
$headers = \substr($this->buffer, 0, $headersSize + 2);
|
||||
$this->buffer = \substr($this->buffer, $headersSize + 4);
|
||||
} else {
|
||||
$headersSize = \strlen($this->buffer);
|
||||
$headers = null;
|
||||
}
|
||||
if ($this->maxHeaderBytes > 0 && $headersSize > $this->maxHeaderBytes) {
|
||||
throw new ParseException("Configured header size exceeded: {$headersSize} bytes received, while the configured limit is {$this->maxHeaderBytes} bytes", Status::REQUEST_HEADER_FIELDS_TOO_LARGE);
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
/**
|
||||
* @param string $rawHeaders
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function parseRawHeaders(string $rawHeaders) : array
|
||||
{
|
||||
// Legacy support for folded headers
|
||||
if (\strpos($rawHeaders, "\r\n ") || \strpos($rawHeaders, "\r\n\t")) {
|
||||
$rawHeaders = \preg_replace("/\r\n[ \t]++/", ' ', $rawHeaders);
|
||||
}
|
||||
try {
|
||||
$headers = Rfc7230::parseRawHeaders($rawHeaders);
|
||||
$headerMap = [];
|
||||
foreach ($headers as [$key, $value]) {
|
||||
$headerMap[\strtolower($key)][] = $value;
|
||||
}
|
||||
} catch (InvalidHeaderException $e) {
|
||||
throw new ParseException('Invalid headers', Status::BAD_REQUEST, $e);
|
||||
}
|
||||
if (isset($headerMap['transfer-encoding'])) {
|
||||
$transferEncodings = \explode(',', \strtolower(\implode(',', $headerMap['transfer-encoding'])));
|
||||
$transferEncodings = \array_map('trim', $transferEncodings);
|
||||
$this->chunkedEncoding = \in_array('chunked', $transferEncodings, \true);
|
||||
} elseif (isset($headerMap['content-length'])) {
|
||||
if (\count($headerMap['content-length']) > 1) {
|
||||
throw new ParseException('Can\'t determine body length, because multiple content-length headers present in the response', Status::BAD_REQUEST);
|
||||
}
|
||||
$contentLength = $headerMap['content-length'][0];
|
||||
if (!\preg_match('/^(0|[1-9][0-9]*)$/', $contentLength)) {
|
||||
throw new ParseException('Can\'t determine body length, because the content-length header value is invalid', Status::BAD_REQUEST);
|
||||
}
|
||||
$this->remainingBodyBytes = (int) $contentLength;
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
/**
|
||||
* Decodes a chunked response body.
|
||||
*
|
||||
* @return bool Returns {@code true} if the body is complete, otherwise {@code false}.
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function parseChunkedBody() : bool
|
||||
{
|
||||
if ($this->chunkLengthRemaining !== null) {
|
||||
goto decode_chunk;
|
||||
}
|
||||
determine_chunk_size:
|
||||
if (\false === ($lineEndPos = \strpos($this->buffer, "\r\n"))) {
|
||||
return \false;
|
||||
}
|
||||
if ($lineEndPos === 0) {
|
||||
throw new ParseException('Invalid line; hexadecimal chunk size expected', Status::BAD_REQUEST);
|
||||
}
|
||||
$line = \substr($this->buffer, 0, $lineEndPos);
|
||||
$hex = \strtolower(\trim(\ltrim($line, '0'))) ?: '0';
|
||||
$dec = \hexdec($hex);
|
||||
if ($hex !== \dechex($dec)) {
|
||||
throw new ParseException('Invalid hexadecimal chunk size', Status::BAD_REQUEST);
|
||||
}
|
||||
$this->chunkLengthRemaining = $dec;
|
||||
$this->buffer = \substr($this->buffer, $lineEndPos + 2);
|
||||
if ($this->chunkLengthRemaining === 0) {
|
||||
return \true;
|
||||
}
|
||||
decode_chunk:
|
||||
$bufferLength = \strlen($this->buffer);
|
||||
// These first two (extreme) edge cases prevent errors where the packet boundary ends after
|
||||
// the \r and before the \n at the end of a chunk.
|
||||
if ($bufferLength === $this->chunkLengthRemaining || $bufferLength === $this->chunkLengthRemaining + 1) {
|
||||
return \false;
|
||||
}
|
||||
if ($bufferLength >= $this->chunkLengthRemaining + 2) {
|
||||
$chunk = \substr($this->buffer, 0, $this->chunkLengthRemaining);
|
||||
$this->buffer = \substr($this->buffer, $this->chunkLengthRemaining + 2);
|
||||
$this->chunkLengthRemaining = null;
|
||||
$this->addToBody($chunk);
|
||||
goto determine_chunk_size;
|
||||
}
|
||||
/** @noinspection SuspiciousAssignmentsInspection */
|
||||
$chunk = $this->buffer;
|
||||
$this->buffer = '';
|
||||
$this->chunkLengthRemaining -= $bufferLength;
|
||||
$this->addToBody($chunk);
|
||||
return \false;
|
||||
}
|
||||
/**
|
||||
* @param string $trailers
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function parseTrailers(string $trailers) : void
|
||||
{
|
||||
try {
|
||||
$trailers = Rfc7230::parseHeaders($trailers);
|
||||
} catch (InvalidHeaderException $e) {
|
||||
throw new ParseException('Invalid trailers', Status::BAD_REQUEST, $e);
|
||||
}
|
||||
($this->trailersCallback)($trailers);
|
||||
}
|
||||
/**
|
||||
* @param string $data
|
||||
*
|
||||
* @throws ParseException
|
||||
*/
|
||||
private function addToBody(string $data) : void
|
||||
{
|
||||
$length = \strlen($data);
|
||||
if (!$length) {
|
||||
return;
|
||||
}
|
||||
$this->bodyBytesConsumed += $length;
|
||||
if ($this->maxBodyBytes > 0 && $this->bodyBytesConsumed > $this->maxBodyBytes) {
|
||||
throw new ParseException("Configured body size exceeded: {$this->bodyBytesConsumed} bytes received, while the configured limit is {$this->maxBodyBytes} bytes", Status::PAYLOAD_TOO_LARGE);
|
||||
}
|
||||
if ($this->bodyDataCallback) {
|
||||
($this->bodyDataCallback)($data);
|
||||
}
|
||||
}
|
||||
}
|
1143
dependencies/amphp/http-client/src/Connection/Internal/Http2ConnectionProcessor.php
vendored
Normal file
1143
dependencies/amphp/http-client/src/Connection/Internal/Http2ConnectionProcessor.php
vendored
Normal file
File diff suppressed because it is too large
Load Diff
107
dependencies/amphp/http-client/src/Connection/Internal/Http2Stream.php
vendored
Normal file
107
dependencies/amphp/http-client/src/Connection/Internal/Http2Stream.php
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Deferred;
|
||||
use WP_Ultimo\Dependencies\Amp\Emitter;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Loop;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Struct;
|
||||
/**
|
||||
* Used in Http2Connection.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Http2Stream
|
||||
{
|
||||
use Struct;
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
/** @var int */
|
||||
public $id;
|
||||
/** @var Request */
|
||||
public $request;
|
||||
/** @var Response|null */
|
||||
public $response;
|
||||
/** @var Deferred|null */
|
||||
public $pendingResponse;
|
||||
/** @var Promise|null */
|
||||
public $preResponseResolution;
|
||||
/** @var bool */
|
||||
public $responsePending = \true;
|
||||
/** @var Emitter|null */
|
||||
public $body;
|
||||
/** @var Deferred|null */
|
||||
public $trailers;
|
||||
/** @var CancellationToken */
|
||||
public $originalCancellation;
|
||||
/** @var CancellationToken */
|
||||
public $cancellationToken;
|
||||
/** @var int Bytes received on the stream. */
|
||||
public $received = 0;
|
||||
/** @var int */
|
||||
public $serverWindow;
|
||||
/** @var int */
|
||||
public $clientWindow;
|
||||
/** @var int */
|
||||
public $bufferSize;
|
||||
/** @var string */
|
||||
public $requestBodyBuffer = '';
|
||||
/** @var bool */
|
||||
public $requestBodyComplete = \false;
|
||||
/** @var Deferred */
|
||||
public $requestBodyCompletion;
|
||||
/** @var int Integer between 1 and 256 */
|
||||
public $weight = 16;
|
||||
/** @var int */
|
||||
public $dependency = 0;
|
||||
/** @var int|null */
|
||||
public $expectedLength;
|
||||
/** @var Stream */
|
||||
public $stream;
|
||||
/** @var Deferred|null */
|
||||
public $windowSizeIncrease;
|
||||
/** @var string|null */
|
||||
private $watcher;
|
||||
public function __construct(int $id, Request $request, Stream $stream, CancellationToken $cancellationToken, CancellationToken $originalCancellation, ?string $watcher, int $serverSize, int $clientSize)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->request = $request;
|
||||
$this->stream = $stream;
|
||||
$this->cancellationToken = $cancellationToken;
|
||||
$this->originalCancellation = $originalCancellation;
|
||||
$this->watcher = $watcher;
|
||||
$this->serverWindow = $serverSize;
|
||||
$this->clientWindow = $clientSize;
|
||||
$this->pendingResponse = new Deferred();
|
||||
$this->requestBodyCompletion = new Deferred();
|
||||
$this->bufferSize = 0;
|
||||
}
|
||||
public function __destruct()
|
||||
{
|
||||
if ($this->watcher !== null) {
|
||||
Loop::cancel($this->watcher);
|
||||
}
|
||||
}
|
||||
public function disableInactivityWatcher() : void
|
||||
{
|
||||
if ($this->watcher === null) {
|
||||
return;
|
||||
}
|
||||
Loop::disable($this->watcher);
|
||||
}
|
||||
public function enableInactivityWatcher() : void
|
||||
{
|
||||
if ($this->watcher === null) {
|
||||
return;
|
||||
}
|
||||
Loop::disable($this->watcher);
|
||||
Loop::enable($this->watcher);
|
||||
}
|
||||
}
|
71
dependencies/amphp/http-client/src/Connection/Internal/RequestNormalizer.php
vendored
Normal file
71
dependencies/amphp/http-client/src/Connection/Internal/RequestNormalizer.php
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\RequestBody;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
/** @internal */
|
||||
final class RequestNormalizer
|
||||
{
|
||||
public static function normalizeRequest(Request $request) : Promise
|
||||
{
|
||||
return call(static function () use($request) {
|
||||
/** @var array $headers */
|
||||
$headers = (yield $request->getBody()->getHeaders());
|
||||
foreach ($headers as $name => $header) {
|
||||
if (!$request->hasHeader($name)) {
|
||||
$request->setHeaders([$name => $header]);
|
||||
}
|
||||
}
|
||||
yield from self::normalizeRequestBodyHeaders($request);
|
||||
// Always normalize this as last item, because we need to strip sensitive headers
|
||||
self::normalizeTraceRequest($request);
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
private static function normalizeRequestBodyHeaders(Request $request) : \Generator
|
||||
{
|
||||
if (!$request->hasHeader('host')) {
|
||||
// Though servers are supposed to be able to handle standard port names on the end of the
|
||||
// Host header some fail to do this correctly. Thankfully PSR-7 recommends to strip the port
|
||||
// if it is the standard port for the given scheme.
|
||||
$request->setHeader('host', $request->getUri()->withUserInfo('')->getAuthority());
|
||||
}
|
||||
if ($request->hasHeader("transfer-encoding")) {
|
||||
$request->removeHeader("content-length");
|
||||
return;
|
||||
}
|
||||
if ($request->hasHeader("content-length")) {
|
||||
return;
|
||||
}
|
||||
/** @var RequestBody $body */
|
||||
$body = $request->getBody();
|
||||
$bodyLength = (yield $body->getBodyLength());
|
||||
if ($bodyLength === 0) {
|
||||
if (\in_array($request->getMethod(), ['HEAD', 'GET', 'CONNECT'], \true)) {
|
||||
$request->removeHeader('content-length');
|
||||
} else {
|
||||
$request->setHeader('content-length', '0');
|
||||
}
|
||||
$request->removeHeader('transfer-encoding');
|
||||
} elseif ($bodyLength > 0) {
|
||||
$request->setHeader("content-length", $bodyLength);
|
||||
$request->removeHeader("transfer-encoding");
|
||||
} else {
|
||||
$request->setHeader("transfer-encoding", "chunked");
|
||||
}
|
||||
}
|
||||
private static function normalizeTraceRequest(Request $request) : void
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
if ($method !== 'TRACE') {
|
||||
return;
|
||||
}
|
||||
// https://tools.ietf.org/html/rfc7231#section-4.3.8
|
||||
$request->setBody(null);
|
||||
// Remove all body and sensitive headers
|
||||
$request->setHeaders(["transfer-encoding" => [], "content-length" => [], "authorization" => [], "proxy-authorization" => [], "cookie" => []]);
|
||||
}
|
||||
}
|
6
dependencies/amphp/http-client/src/Connection/LimitedConnectionPool.php
vendored
Normal file
6
dependencies/amphp/http-client/src/Connection/LimitedConnectionPool.php
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
// Alias for backward compatibility.
|
||||
\class_alias(StreamLimitingPool::class, LimitedConnectionPool::class);
|
37
dependencies/amphp/http-client/src/Connection/Stream.php
vendored
Normal file
37
dependencies/amphp/http-client/src/Connection/Stream.php
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\EventListener;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
interface Stream extends DelegateHttpClient
|
||||
{
|
||||
/**
|
||||
* Executes the request.
|
||||
*
|
||||
* This method may only be invoked once per instance.
|
||||
*
|
||||
* The stream must call {@see EventListener::startSendingRequest()},
|
||||
* {@see EventListener::completeSendingRequest()}, {@see EventListener::startReceivingResponse()}, and
|
||||
* {@see EventListener::completeReceivingResponse()} event listener methods on all event listeners registered on
|
||||
* the given request in the order defined by {@see Request::getEventListeners()}. Before calling the next listener,
|
||||
* the promise returned from the previous one must resolve successfully.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
*
|
||||
* @return Promise<Response>
|
||||
*
|
||||
* @throws \Error Thrown if this method is called more than once.
|
||||
*/
|
||||
public function request(Request $request, CancellationToken $cancellation) : Promise;
|
||||
public function getLocalAddress() : SocketAddress;
|
||||
public function getRemoteAddress() : SocketAddress;
|
||||
public function getTlsInfo() : ?TlsInfo;
|
||||
}
|
72
dependencies/amphp/http-client/src/Connection/StreamLimitingPool.php
vendored
Normal file
72
dependencies/amphp/http-client/src/Connection/StreamLimitingPool.php
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Sync\KeyedSemaphore;
|
||||
use WP_Ultimo\Dependencies\Amp\Sync\Lock;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
use function WP_Ultimo\Dependencies\Amp\coroutine;
|
||||
final class StreamLimitingPool implements ConnectionPool
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
public static function byHost(ConnectionPool $delegate, KeyedSemaphore $semaphore) : self
|
||||
{
|
||||
return new self($delegate, $semaphore, static function (Request $request) {
|
||||
return $request->getUri()->getHost();
|
||||
});
|
||||
}
|
||||
public static function byStaticKey(ConnectionPool $delegate, KeyedSemaphore $semaphore, string $key = '') : self
|
||||
{
|
||||
return new self($delegate, $semaphore, static function () use($key) {
|
||||
return $key;
|
||||
});
|
||||
}
|
||||
public static function byCustomKey(ConnectionPool $delegate, KeyedSemaphore $semaphore, callable $requestToKeyMapper) : self
|
||||
{
|
||||
return new self($delegate, $semaphore, $requestToKeyMapper);
|
||||
}
|
||||
/** @var ConnectionPool */
|
||||
private $delegate;
|
||||
/** @var KeyedSemaphore */
|
||||
private $semaphore;
|
||||
/** @var callable */
|
||||
private $requestToKeyMapper;
|
||||
private function __construct(ConnectionPool $delegate, KeyedSemaphore $semaphore, callable $requestToKeyMapper)
|
||||
{
|
||||
$this->delegate = $delegate;
|
||||
$this->semaphore = $semaphore;
|
||||
$this->requestToKeyMapper = $requestToKeyMapper;
|
||||
}
|
||||
public function getStream(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation) {
|
||||
/** @var Lock $lock */
|
||||
$lock = (yield $this->semaphore->acquire(($this->requestToKeyMapper)($request)));
|
||||
/** @var Stream $stream */
|
||||
$stream = (yield $this->delegate->getStream($request, $cancellation));
|
||||
return HttpStream::fromStream($stream, coroutine(static function (Request $request, CancellationToken $cancellationToken) use($stream, $lock) {
|
||||
try {
|
||||
/** @var Response $response */
|
||||
$response = (yield $stream->request($request, $cancellationToken));
|
||||
// await response being completely received
|
||||
$response->getTrailers()->onResolve(static function () use($lock) {
|
||||
$lock->release();
|
||||
});
|
||||
} catch (\Throwable $e) {
|
||||
$lock->release();
|
||||
throw $e;
|
||||
}
|
||||
return $response;
|
||||
}), static function () use($lock) {
|
||||
$lock->release();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
38
dependencies/amphp/http-client/src/Connection/UnlimitedConnectionPool.php
vendored
Normal file
38
dependencies/amphp/http-client/src/Connection/UnlimitedConnectionPool.php
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
final class UnlimitedConnectionPool implements ConnectionPool
|
||||
{
|
||||
use ForbidSerialization;
|
||||
/** @var ConnectionLimitingPool */
|
||||
private $pool;
|
||||
public function __construct(?ConnectionFactory $connectionFactory = null)
|
||||
{
|
||||
$this->pool = ConnectionLimitingPool::byAuthority(\PHP_INT_MAX, $connectionFactory);
|
||||
}
|
||||
public function __clone()
|
||||
{
|
||||
$this->pool = clone $this->pool;
|
||||
}
|
||||
public function getTotalConnectionAttempts() : int
|
||||
{
|
||||
return $this->pool->getTotalConnectionAttempts();
|
||||
}
|
||||
public function getTotalStreamRequests() : int
|
||||
{
|
||||
return $this->pool->getTotalStreamRequests();
|
||||
}
|
||||
public function getOpenConnectionCount() : int
|
||||
{
|
||||
return $this->pool->getOpenConnectionCount();
|
||||
}
|
||||
public function getStream(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
return $this->pool->getStream($request, $cancellation);
|
||||
}
|
||||
}
|
12
dependencies/amphp/http-client/src/Connection/UnprocessedRequestException.php
vendored
Normal file
12
dependencies/amphp/http-client/src/Connection/UnprocessedRequestException.php
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\HttpException;
|
||||
final class UnprocessedRequestException extends HttpException
|
||||
{
|
||||
public function __construct(HttpException $previous)
|
||||
{
|
||||
parent::__construct("The request was not processed and can be safely retried", 0, $previous);
|
||||
}
|
||||
}
|
91
dependencies/amphp/http-client/src/Connection/UpgradedSocket.php
vendored
Normal file
91
dependencies/amphp/http-client/src/Connection/UpgradedSocket.php
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Connection;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\EncryptableSocket;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\SocketAddress;
|
||||
use WP_Ultimo\Dependencies\Amp\Socket\TlsInfo;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
final class UpgradedSocket implements EncryptableSocket
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var EncryptableSocket */
|
||||
private $socket;
|
||||
/** @var string|null */
|
||||
private $buffer;
|
||||
/**
|
||||
* @param EncryptableSocket $socket
|
||||
* @param string $buffer Remaining buffer previously read from the socket.
|
||||
*/
|
||||
public function __construct(EncryptableSocket $socket, string $buffer)
|
||||
{
|
||||
$this->socket = $socket;
|
||||
$this->buffer = $buffer !== '' ? $buffer : null;
|
||||
}
|
||||
public function read() : Promise
|
||||
{
|
||||
if ($this->buffer !== null) {
|
||||
$buffer = $this->buffer;
|
||||
$this->buffer = null;
|
||||
return new Success($buffer);
|
||||
}
|
||||
return $this->socket->read();
|
||||
}
|
||||
public function close() : void
|
||||
{
|
||||
$this->socket->close();
|
||||
}
|
||||
public function __destruct()
|
||||
{
|
||||
$this->close();
|
||||
}
|
||||
public function write(string $data) : Promise
|
||||
{
|
||||
return $this->socket->write($data);
|
||||
}
|
||||
public function end(string $finalData = "") : Promise
|
||||
{
|
||||
return $this->socket->end($finalData);
|
||||
}
|
||||
public function reference() : void
|
||||
{
|
||||
$this->socket->reference();
|
||||
}
|
||||
public function unreference() : void
|
||||
{
|
||||
$this->socket->unreference();
|
||||
}
|
||||
public function isClosed() : bool
|
||||
{
|
||||
return $this->socket->isClosed();
|
||||
}
|
||||
public function getLocalAddress() : SocketAddress
|
||||
{
|
||||
return $this->socket->getLocalAddress();
|
||||
}
|
||||
public function getRemoteAddress() : SocketAddress
|
||||
{
|
||||
return $this->socket->getRemoteAddress();
|
||||
}
|
||||
public function setupTls(?CancellationToken $cancellationToken = null) : Promise
|
||||
{
|
||||
return $this->socket->setupTls($cancellationToken);
|
||||
}
|
||||
public function shutdownTls(?CancellationToken $cancellationToken = null) : Promise
|
||||
{
|
||||
return $this->socket->shutdownTls();
|
||||
}
|
||||
public function getTlsState() : int
|
||||
{
|
||||
return $this->socket->getTlsState();
|
||||
}
|
||||
public function getTlsInfo() : ?TlsInfo
|
||||
{
|
||||
return $this->socket->getTlsInfo();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user