Initial Commit
This commit is contained in:
34
dependencies/amphp/http-client/src/ApplicationInterceptor.php
vendored
Normal file
34
dependencies/amphp/http-client/src/ApplicationInterceptor.php
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor\DecompressResponse;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/**
|
||||
* Allows intercepting an application request to an HTTP resource.
|
||||
*/
|
||||
interface ApplicationInterceptor
|
||||
{
|
||||
/**
|
||||
* Intercepts an application request to an HTTP resource.
|
||||
*
|
||||
* The implementation might modify the request, delegate the request handling to the `$httpClient`, and/or modify
|
||||
* the response after the promise returned from `$httpClient->request(...)` resolves.
|
||||
*
|
||||
* An interceptor might also short-circuit and not delegate to the `$httpClient` at all.
|
||||
*
|
||||
* Any retry or cloned follow-up request must be manually cloned from `$request` to ensure a properly working
|
||||
* interceptor chain, e.g. the {@see DecompressResponse} interceptor only decodes a response if the
|
||||
* `accept-encoding` header isn't set manually. If the request isn't cloned, the first attempt will set the header
|
||||
* and the second attempt will see the header and won't decode the response, because it thinks another interceptor
|
||||
* or the application itself will care about the decoding.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
* @param DelegateHttpClient $httpClient
|
||||
*
|
||||
* @return Promise<Response>
|
||||
*/
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise;
|
||||
}
|
69
dependencies/amphp/http-client/src/Body/FileBody.php
vendored
Normal file
69
dependencies/amphp/http-client/src/Body/FileBody.php
vendored
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Body;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\File\Driver;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\RequestBody;
|
||||
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\File\getSize;
|
||||
use function WP_Ultimo\Dependencies\Amp\File\open;
|
||||
use function WP_Ultimo\Dependencies\Amp\File\openFile;
|
||||
use function WP_Ultimo\Dependencies\Amp\File\size;
|
||||
final class FileBody implements RequestBody
|
||||
{
|
||||
/** @var string */
|
||||
private $path;
|
||||
/**
|
||||
* @param string $path The filesystem path for the file we wish to send
|
||||
*/
|
||||
public function __construct(string $path)
|
||||
{
|
||||
if (!\interface_exists(Driver::class)) {
|
||||
throw new \Error("File request bodies require amphp/file to be installed");
|
||||
}
|
||||
$this->path = $path;
|
||||
}
|
||||
public function createBodyStream() : InputStream
|
||||
{
|
||||
$handlePromise = \function_exists('WP_Ultimo\\Dependencies\\Amp\\File\\openFile') ? openFile($this->path, "r") : open($this->path, "r");
|
||||
return new class($handlePromise) implements InputStream
|
||||
{
|
||||
/** @var Promise<InputStream> */
|
||||
private $promise;
|
||||
/** @var InputStream|null */
|
||||
private $stream;
|
||||
public function __construct(Promise $promise)
|
||||
{
|
||||
$this->promise = $promise;
|
||||
$this->promise->onResolve(function ($error, $stream) {
|
||||
if ($error) {
|
||||
return;
|
||||
}
|
||||
$this->stream = $stream;
|
||||
});
|
||||
}
|
||||
public function read() : Promise
|
||||
{
|
||||
if (!$this->stream) {
|
||||
return call(function () {
|
||||
/** @var InputStream $stream */
|
||||
$stream = (yield $this->promise);
|
||||
return $stream->read();
|
||||
});
|
||||
}
|
||||
return $this->stream->read();
|
||||
}
|
||||
};
|
||||
}
|
||||
public function getHeaders() : Promise
|
||||
{
|
||||
return new Success([]);
|
||||
}
|
||||
public function getBodyLength() : Promise
|
||||
{
|
||||
return \function_exists('WP_Ultimo\\Dependencies\\Amp\\File\\getSize') ? getSize($this->path) : size($this->path);
|
||||
}
|
||||
}
|
218
dependencies/amphp/http-client/src/Body/FormBody.php
vendored
Normal file
218
dependencies/amphp/http-client/src/Body/FormBody.php
vendored
Normal file
@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Body;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InMemoryStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\IteratorStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\RequestBody;
|
||||
use WP_Ultimo\Dependencies\Amp\Producer;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class FormBody implements RequestBody
|
||||
{
|
||||
/** @var (array{0: string, 1: string, 2: string, 3: null}|array{0: string, 1: FileBody, 2: string, 3: string})[] */
|
||||
private $fields = [];
|
||||
/** @var string */
|
||||
private $boundary;
|
||||
/** @var bool */
|
||||
private $isMultipart = \false;
|
||||
/** @var string|null */
|
||||
private $cachedBody;
|
||||
/** @var Promise<int>|null */
|
||||
private $cachedLength;
|
||||
/** @var list<string|FileBody>|null */
|
||||
private $cachedFields;
|
||||
/**
|
||||
* @param string $boundary An optional multipart boundary string
|
||||
*/
|
||||
public function __construct(string $boundary = null)
|
||||
{
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$this->boundary = $boundary ?? \bin2hex(\random_bytes(16));
|
||||
}
|
||||
/**
|
||||
* Add a data field to the form entity body.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @param string $contentType
|
||||
*/
|
||||
public function addField(string $name, string $value, string $contentType = 'text/plain') : void
|
||||
{
|
||||
$this->fields[] = [$name, $value, $contentType, null];
|
||||
$this->resetCache();
|
||||
}
|
||||
/**
|
||||
* Add each element of a associative array as a data field to the form entity body.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $contentType
|
||||
*/
|
||||
public function addFields(array $data, string $contentType = 'text/plain') : void
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$this->addField($key, $value, $contentType);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a file field to the form entity body.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $filePath
|
||||
* @param string $contentType
|
||||
*/
|
||||
public function addFile(string $name, string $filePath, string $contentType = 'application/octet-stream') : void
|
||||
{
|
||||
$fileName = \basename($filePath);
|
||||
$this->fields[] = [$name, new FileBody($filePath), $contentType, $fileName];
|
||||
$this->isMultipart = \true;
|
||||
$this->resetCache();
|
||||
}
|
||||
/**
|
||||
* Add each element of a associative array as a file field to the form entity body.
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $contentType
|
||||
*/
|
||||
public function addFiles(array $data, string $contentType = 'application/octet-stream') : void
|
||||
{
|
||||
foreach ($data as $key => $value) {
|
||||
$this->addFile($key, $value, $contentType);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a file field to the form from a string.
|
||||
*
|
||||
* @param string $name
|
||||
* @param string $fileContent
|
||||
* @param string $fileName
|
||||
* @param string $contentType
|
||||
*/
|
||||
public function addFileFromString(string $name, string $fileContent, string $fileName, string $contentType = 'application/octet-stream') : void
|
||||
{
|
||||
$this->fields[] = [$name, $fileContent, $contentType, $fileName];
|
||||
$this->isMultipart = \true;
|
||||
$this->resetCache();
|
||||
}
|
||||
/**
|
||||
* Returns an array of fields, each being an array of [name, value, content-type, file-name|null].
|
||||
* Both fields and files are returned in the array. Files use a FileBody object as the value. The file-name is
|
||||
* always null for fields.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFields() : array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
private function resetCache() : void
|
||||
{
|
||||
$this->cachedBody = null;
|
||||
$this->cachedLength = null;
|
||||
$this->cachedFields = null;
|
||||
}
|
||||
public function createBodyStream() : InputStream
|
||||
{
|
||||
if ($this->isMultipart) {
|
||||
return $this->generateMultipartStreamFromFields($this->getMultipartFieldArray());
|
||||
}
|
||||
return new InMemoryStream($this->getFormEncodedBodyString());
|
||||
}
|
||||
private function getMultipartFieldArray() : array
|
||||
{
|
||||
if (isset($this->cachedFields)) {
|
||||
return $this->cachedFields;
|
||||
}
|
||||
$fields = [];
|
||||
foreach ($this->fields as $fieldArr) {
|
||||
[$name, $field, $contentType, $fileName] = $fieldArr;
|
||||
$fields[] = "--{$this->boundary}\r\n";
|
||||
/** @psalm-suppress PossiblyNullArgument */
|
||||
$fields[] = $fileName !== null ? $this->generateMultipartFileHeader($name, $fileName, $contentType) : $this->generateMultipartFieldHeader($name, $contentType);
|
||||
$fields[] = $field;
|
||||
$fields[] = "\r\n";
|
||||
}
|
||||
$fields[] = "--{$this->boundary}--\r\n";
|
||||
return $this->cachedFields = $fields;
|
||||
}
|
||||
private function generateMultipartFileHeader(string $name, string $fileName, string $contentType) : string
|
||||
{
|
||||
$header = "Content-Disposition: form-data; name=\"{$name}\"; filename=\"{$fileName}\"\r\n";
|
||||
$header .= "Content-Type: {$contentType}\r\n";
|
||||
$header .= "Content-Transfer-Encoding: binary\r\n\r\n";
|
||||
return $header;
|
||||
}
|
||||
private function generateMultipartFieldHeader(string $name, string $contentType) : string
|
||||
{
|
||||
$header = "Content-Disposition: form-data; name=\"{$name}\"\r\n";
|
||||
if ($contentType !== "") {
|
||||
$header .= "Content-Type: {$contentType}\r\n\r\n";
|
||||
} else {
|
||||
$header .= "\r\n";
|
||||
}
|
||||
return $header;
|
||||
}
|
||||
private function generateMultipartStreamFromFields(array $fields) : InputStream
|
||||
{
|
||||
foreach ($fields as $key => $field) {
|
||||
$fields[$key] = $field instanceof FileBody ? $field->createBodyStream() : new InMemoryStream($field);
|
||||
}
|
||||
return new IteratorStream(new Producer(static function (callable $emit) use($fields) {
|
||||
/** @psalm-var callable(string) $emit */
|
||||
foreach ($fields as $stream) {
|
||||
/** @var InputStream $stream */
|
||||
while (null !== ($chunk = (yield $stream->read()))) {
|
||||
(yield $emit($chunk));
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
private function getFormEncodedBodyString() : string
|
||||
{
|
||||
if ($this->cachedBody) {
|
||||
return $this->cachedBody;
|
||||
}
|
||||
$fields = [];
|
||||
foreach ($this->fields as $fieldArr) {
|
||||
[$name, $value] = $fieldArr;
|
||||
$fields[$name][] = $value;
|
||||
}
|
||||
foreach ($fields as $key => $value) {
|
||||
$fields[$key] = isset($value[1]) ? $value : $value[0];
|
||||
}
|
||||
return $this->cachedBody = \http_build_query($fields);
|
||||
}
|
||||
public function getHeaders() : Promise
|
||||
{
|
||||
return new Success(['Content-Type' => $this->determineContentType()]);
|
||||
}
|
||||
private function determineContentType() : string
|
||||
{
|
||||
return $this->isMultipart ? "multipart/form-data; boundary={$this->boundary}" : 'application/x-www-form-urlencoded';
|
||||
}
|
||||
public function getBodyLength() : Promise
|
||||
{
|
||||
if ($this->cachedLength) {
|
||||
return $this->cachedLength;
|
||||
}
|
||||
if (!$this->isMultipart) {
|
||||
return $this->cachedLength = new Success(\strlen($this->getFormEncodedBodyString()));
|
||||
}
|
||||
/** @var Promise<int> $lengthPromise */
|
||||
$lengthPromise = call(function () : \Generator {
|
||||
$fields = $this->getMultipartFieldArray();
|
||||
$length = 0;
|
||||
foreach ($fields as $field) {
|
||||
if (\is_string($field)) {
|
||||
$length += \strlen($field);
|
||||
} else {
|
||||
$length += (yield $field->getBodyLength());
|
||||
}
|
||||
}
|
||||
return $length;
|
||||
});
|
||||
return $this->cachedLength = $lengthPromise;
|
||||
}
|
||||
}
|
44
dependencies/amphp/http-client/src/Body/JsonBody.php
vendored
Normal file
44
dependencies/amphp/http-client/src/Body/JsonBody.php
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Body;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InMemoryStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\HttpException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\RequestBody;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
final class JsonBody implements RequestBody
|
||||
{
|
||||
/** @var string */
|
||||
private $json;
|
||||
/**
|
||||
* JsonBody constructor.
|
||||
*
|
||||
* @param mixed $data
|
||||
* @param int $options
|
||||
* @param int<1, 2147483647> $depth
|
||||
*
|
||||
* @throws HttpException
|
||||
*/
|
||||
public function __construct($data, int $options = 0, int $depth = 512)
|
||||
{
|
||||
$this->json = \json_encode($data, $options, $depth);
|
||||
if (\json_last_error() !== \JSON_ERROR_NONE) {
|
||||
throw new HttpException('Failed to encode data to JSON');
|
||||
}
|
||||
}
|
||||
public function getHeaders() : Promise
|
||||
{
|
||||
return new Success(['content-type' => 'application/json; charset=utf-8']);
|
||||
}
|
||||
public function createBodyStream() : InputStream
|
||||
{
|
||||
return new InMemoryStream($this->json);
|
||||
}
|
||||
public function getBodyLength() : Promise
|
||||
{
|
||||
return new Success(\strlen($this->json));
|
||||
}
|
||||
}
|
29
dependencies/amphp/http-client/src/Body/StringBody.php
vendored
Normal file
29
dependencies/amphp/http-client/src/Body/StringBody.php
vendored
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Body;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InMemoryStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\RequestBody;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
final class StringBody implements RequestBody
|
||||
{
|
||||
private $body;
|
||||
public function __construct(string $body)
|
||||
{
|
||||
$this->body = $body;
|
||||
}
|
||||
public function createBodyStream() : InputStream
|
||||
{
|
||||
return new InMemoryStream($this->body !== '' ? $this->body : null);
|
||||
}
|
||||
public function getHeaders() : Promise
|
||||
{
|
||||
return new Success([]);
|
||||
}
|
||||
public function getBodyLength() : Promise
|
||||
{
|
||||
return new Success(\strlen($this->body));
|
||||
}
|
||||
}
|
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();
|
||||
}
|
||||
}
|
31
dependencies/amphp/http-client/src/DelegateHttpClient.php
vendored
Normal file
31
dependencies/amphp/http-client/src/DelegateHttpClient.php
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/**
|
||||
* Base HTTP client interface for use in {@see ApplicationInterceptor}.
|
||||
*
|
||||
* Applications and implementations should depend on {@see HttpClient} instead. The intent of this interface is to
|
||||
* allow static analysis tools to find interceptors that forget to pass the cancellation token down. This situation is
|
||||
* created because of the cancellation token being optional.
|
||||
*
|
||||
* Before executing or delegating the request, any client implementation must call {@see EventListener::startRequest()}
|
||||
* 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.
|
||||
*
|
||||
* @see HttpClient
|
||||
*/
|
||||
interface DelegateHttpClient
|
||||
{
|
||||
/**
|
||||
* Request a specific resource from an HTTP server.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
*
|
||||
* @return Promise<Response>
|
||||
*/
|
||||
public function request(Request $request, CancellationToken $cancellation) : Promise;
|
||||
}
|
117
dependencies/amphp/http-client/src/EventListener.php
vendored
Normal file
117
dependencies/amphp/http-client/src/EventListener.php
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\ConnectionPool;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/**
|
||||
* Allows listening to more fine granular events than interceptors are able to achieve.
|
||||
*
|
||||
* All event listener methods might be called multiple times for a single request. The implementing listener is
|
||||
* responsible to detect another call, e.g. via attributes in the request.
|
||||
*/
|
||||
interface EventListener
|
||||
{
|
||||
/**
|
||||
* Called at the very beginning of {@see DelegateHttpClient::request()}.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function startRequest(Request $request) : Promise;
|
||||
/**
|
||||
* Optionally called by {@see ConnectionPool::getStream()} before DNS resolution is started.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function startDnsResolution(Request $request) : Promise;
|
||||
/**
|
||||
* Optionally called by {@see ConnectionPool::getStream()} after DNS resolution is completed.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function completeDnsResolution(Request $request) : Promise;
|
||||
/**
|
||||
* Called by {@see ConnectionPool::getStream()} before a new connection is initiated.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function startConnectionCreation(Request $request) : Promise;
|
||||
/**
|
||||
* Called by {@see ConnectionPool::getStream()} after a new connection is established and TLS negotiated.
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function completeConnectionCreation(Request $request) : Promise;
|
||||
/**
|
||||
* Called by {@see ConnectionPool::getStream()} before TLS negotiation is started (only if HTTPS is used).
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function startTlsNegotiation(Request $request) : Promise;
|
||||
/**
|
||||
* Called by {@see ConnectionPool::getStream()} after TLS negotiation is successful (only if HTTPS is used).
|
||||
*
|
||||
* @param Request $request
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function completeTlsNegotiation(Request $request) : Promise;
|
||||
/**
|
||||
* Called by {@see Stream::request()} before the request is sent.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function startSendingRequest(Request $request, Stream $stream) : Promise;
|
||||
/**
|
||||
* Called by {@see Stream::request()} after the request is sent.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function completeSendingRequest(Request $request, Stream $stream) : Promise;
|
||||
/**
|
||||
* Called by {@see Stream::request()} after the first response byte is received.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function startReceivingResponse(Request $request, Stream $stream) : Promise;
|
||||
/**
|
||||
* Called by {@see Stream::request()} after the request is complete.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return Promise Should resolve successfully, otherwise aborts the request.
|
||||
*/
|
||||
public function completeReceivingResponse(Request $request, Stream $stream) : Promise;
|
||||
/**
|
||||
* Called if the request is aborted.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param \Throwable $cause
|
||||
*
|
||||
* @return Promise Should resolve successfully.
|
||||
*/
|
||||
public function abort(Request $request, \Throwable $cause) : Promise;
|
||||
}
|
81
dependencies/amphp/http-client/src/EventListener/RecordHarAttributes.php
vendored
Normal file
81
dependencies/amphp/http-client/src/EventListener/RecordHarAttributes.php
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\EventListener;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\EventListener;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\HarAttributes;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
use function WP_Ultimo\Dependencies\Amp\getCurrentTime;
|
||||
final class RecordHarAttributes implements EventListener
|
||||
{
|
||||
public function startRequest(Request $request) : Promise
|
||||
{
|
||||
if (!$request->hasAttribute(HarAttributes::STARTED_DATE_TIME)) {
|
||||
$request->setAttribute(HarAttributes::STARTED_DATE_TIME, new \DateTimeImmutable());
|
||||
}
|
||||
return $this->addTiming(HarAttributes::TIME_START, $request);
|
||||
}
|
||||
public function startDnsResolution(Request $request) : Promise
|
||||
{
|
||||
return new Success();
|
||||
// not implemented
|
||||
}
|
||||
public function startConnectionCreation(Request $request) : Promise
|
||||
{
|
||||
return $this->addTiming(HarAttributes::TIME_CONNECT, $request);
|
||||
}
|
||||
public function startTlsNegotiation(Request $request) : Promise
|
||||
{
|
||||
return $this->addTiming(HarAttributes::TIME_SSL, $request);
|
||||
}
|
||||
public function startSendingRequest(Request $request, Stream $stream) : Promise
|
||||
{
|
||||
$host = $stream->getRemoteAddress()->getHost();
|
||||
if (\strrpos($host, ':')) {
|
||||
$host = '[' . $host . ']';
|
||||
}
|
||||
$request->setAttribute(HarAttributes::SERVER_IP_ADDRESS, $host);
|
||||
return $this->addTiming(HarAttributes::TIME_SEND, $request);
|
||||
}
|
||||
public function completeSendingRequest(Request $request, Stream $stream) : Promise
|
||||
{
|
||||
return $this->addTiming(HarAttributes::TIME_WAIT, $request);
|
||||
}
|
||||
public function startReceivingResponse(Request $request, Stream $stream) : Promise
|
||||
{
|
||||
return $this->addTiming(HarAttributes::TIME_RECEIVE, $request);
|
||||
}
|
||||
public function completeReceivingResponse(Request $request, Stream $stream) : Promise
|
||||
{
|
||||
return $this->addTiming(HarAttributes::TIME_COMPLETE, $request);
|
||||
}
|
||||
public function completeDnsResolution(Request $request) : Promise
|
||||
{
|
||||
return new Success();
|
||||
// not implemented
|
||||
}
|
||||
public function completeConnectionCreation(Request $request) : Promise
|
||||
{
|
||||
return new Success();
|
||||
// not implemented
|
||||
}
|
||||
public function completeTlsNegotiation(Request $request) : Promise
|
||||
{
|
||||
return new Success();
|
||||
// not implemented
|
||||
}
|
||||
private function addTiming(string $key, Request $request) : Promise
|
||||
{
|
||||
if (!$request->hasAttribute($key)) {
|
||||
$request->setAttribute($key, getCurrentTime());
|
||||
}
|
||||
return new Success();
|
||||
}
|
||||
public function abort(Request $request, \Throwable $cause) : Promise
|
||||
{
|
||||
return new Success();
|
||||
}
|
||||
}
|
31
dependencies/amphp/http-client/src/HttpClient.php
vendored
Normal file
31
dependencies/amphp/http-client/src/HttpClient.php
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\NullCancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/**
|
||||
* Convenient HTTP client for use in applications and libraries, providing a default for the cancellation token and
|
||||
* automatically cloning the passed request, so future application requests can re-use the same object again.
|
||||
*/
|
||||
final class HttpClient implements DelegateHttpClient
|
||||
{
|
||||
private $httpClient;
|
||||
public function __construct(DelegateHttpClient $httpClient)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
}
|
||||
/**
|
||||
* Request a specific resource from an HTTP server.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
*
|
||||
* @return Promise<Response>
|
||||
*/
|
||||
public function request(Request $request, ?CancellationToken $cancellation = null) : Promise
|
||||
{
|
||||
return $this->httpClient->request(clone $request, $cancellation ?? new NullCancellationToken());
|
||||
}
|
||||
}
|
204
dependencies/amphp/http-client/src/HttpClientBuilder.php
vendored
Normal file
204
dependencies/amphp/http-client/src/HttpClientBuilder.php
vendored
Normal file
@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\ConnectionPool;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\UnlimitedConnectionPool;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor\DecompressResponse;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor\FollowRedirects;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor\ForbidUriUserInfo;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor\RetryRequests;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor\SetRequestHeaderIfUnset;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
/**
|
||||
* Allows building an HttpClient instance.
|
||||
*
|
||||
* The builder is the recommended way to build an HttpClient instance.
|
||||
*/
|
||||
final class HttpClientBuilder
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
public static function buildDefault() : HttpClient
|
||||
{
|
||||
return (new self())->build();
|
||||
}
|
||||
/** @var ForbidUriUserInfo|null */
|
||||
private $forbidUriUserInfo;
|
||||
/** @var RetryRequests|null */
|
||||
private $retryInterceptor;
|
||||
/** @var FollowRedirects|null */
|
||||
private $followRedirectsInterceptor;
|
||||
/** @var SetRequestHeaderIfUnset|null */
|
||||
private $defaultUserAgentInterceptor;
|
||||
/** @var SetRequestHeaderIfUnset|null */
|
||||
private $defaultAcceptInterceptor;
|
||||
/** @var NetworkInterceptor|null */
|
||||
private $defaultCompressionHandler;
|
||||
/** @var ApplicationInterceptor[] */
|
||||
private $applicationInterceptors = [];
|
||||
/** @var NetworkInterceptor[] */
|
||||
private $networkInterceptors = [];
|
||||
/** @var ConnectionPool */
|
||||
private $pool;
|
||||
public function __construct()
|
||||
{
|
||||
$this->pool = new UnlimitedConnectionPool();
|
||||
$this->forbidUriUserInfo = new ForbidUriUserInfo();
|
||||
$this->followRedirectsInterceptor = new FollowRedirects(10);
|
||||
$this->retryInterceptor = new RetryRequests(2);
|
||||
$this->defaultAcceptInterceptor = new SetRequestHeaderIfUnset('accept', '*/*');
|
||||
$this->defaultUserAgentInterceptor = new SetRequestHeaderIfUnset('user-agent', 'amphp/http-client @ v4.x');
|
||||
$this->defaultCompressionHandler = new DecompressResponse();
|
||||
}
|
||||
public function build() : HttpClient
|
||||
{
|
||||
/** @var PooledHttpClient $client */
|
||||
$client = new PooledHttpClient($this->pool);
|
||||
foreach ($this->networkInterceptors as $interceptor) {
|
||||
$client = $client->intercept($interceptor);
|
||||
}
|
||||
if ($this->defaultAcceptInterceptor) {
|
||||
$client = $client->intercept($this->defaultAcceptInterceptor);
|
||||
}
|
||||
if ($this->defaultUserAgentInterceptor) {
|
||||
$client = $client->intercept($this->defaultUserAgentInterceptor);
|
||||
}
|
||||
if ($this->defaultCompressionHandler) {
|
||||
$client = $client->intercept($this->defaultCompressionHandler);
|
||||
}
|
||||
$applicationInterceptors = $this->applicationInterceptors;
|
||||
if ($this->followRedirectsInterceptor) {
|
||||
\array_unshift($applicationInterceptors, $this->followRedirectsInterceptor);
|
||||
}
|
||||
if ($this->forbidUriUserInfo) {
|
||||
\array_unshift($applicationInterceptors, $this->forbidUriUserInfo);
|
||||
}
|
||||
if ($this->retryInterceptor) {
|
||||
$applicationInterceptors[] = $this->retryInterceptor;
|
||||
}
|
||||
foreach (\array_reverse($applicationInterceptors) as $applicationInterceptor) {
|
||||
$client = new InterceptedHttpClient($client, $applicationInterceptor);
|
||||
}
|
||||
return new HttpClient($client);
|
||||
}
|
||||
/**
|
||||
* @param ConnectionPool $pool Connection pool to use.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function usingPool(ConnectionPool $pool) : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
$builder->pool = $pool;
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* @param ApplicationInterceptor $interceptor This interceptor gets added to the interceptor queue, so interceptors
|
||||
* are executed in the order given to this method.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function intercept(ApplicationInterceptor $interceptor) : self
|
||||
{
|
||||
if ($this->followRedirectsInterceptor !== null && $interceptor instanceof FollowRedirects) {
|
||||
throw new \Error('Disable automatic redirect following or use HttpClientBuilder::followRedirects() to customize redirects');
|
||||
}
|
||||
if ($this->retryInterceptor !== null && $interceptor instanceof RetryRequests) {
|
||||
throw new \Error('Disable automatic retries or use HttpClientBuilder::retry() to customize retries');
|
||||
}
|
||||
$builder = clone $this;
|
||||
$builder->applicationInterceptors[] = $interceptor;
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* @param NetworkInterceptor $interceptor This interceptor gets added to the interceptor queue, so interceptors
|
||||
* are executed in the order given to this method.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function interceptNetwork(NetworkInterceptor $interceptor) : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
$builder->networkInterceptors[] = $interceptor;
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* @param int $retryLimit Maximum number of times a request may be retried. Only certain requests will be retried
|
||||
* automatically (GET, HEAD, PUT, and DELETE requests are automatically retried, or any
|
||||
* request that was indicated as unprocessed by the connection).
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function retry(int $retryLimit) : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
if ($retryLimit <= 0) {
|
||||
$builder->retryInterceptor = null;
|
||||
} else {
|
||||
$builder->retryInterceptor = new RetryRequests($retryLimit);
|
||||
}
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* @param int $limit Maximum number of redirects to follow. The client will automatically request the URI supplied
|
||||
* by a redirect response (3xx status codes) and returns that response instead.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function followRedirects(int $limit = 10) : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
if ($limit <= 0) {
|
||||
$builder->followRedirectsInterceptor = null;
|
||||
} else {
|
||||
$builder->followRedirectsInterceptor = new FollowRedirects($limit);
|
||||
}
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* Removes the default restriction of user:password in request URIs.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function allowDeprecatedUriUserInfo() : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
$builder->forbidUriUserInfo = null;
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* Doesn't automatically set an 'accept' header.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function skipDefaultAcceptHeader() : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
$builder->defaultAcceptInterceptor = null;
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* Doesn't automatically set a 'user-agent' header.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function skipDefaultUserAgent() : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
$builder->defaultUserAgentInterceptor = null;
|
||||
return $builder;
|
||||
}
|
||||
/**
|
||||
* Doesn't automatically set an 'accept-encoding' header and decompress the response.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function skipAutomaticCompression() : self
|
||||
{
|
||||
$builder = clone $this;
|
||||
$builder->defaultCompressionHandler = null;
|
||||
return $builder;
|
||||
}
|
||||
}
|
7
dependencies/amphp/http-client/src/HttpException.php
vendored
Normal file
7
dependencies/amphp/http-client/src/HttpException.php
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
class HttpException extends \Exception
|
||||
{
|
||||
}
|
32
dependencies/amphp/http-client/src/InterceptedHttpClient.php
vendored
Normal file
32
dependencies/amphp/http-client/src/InterceptedHttpClient.php
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
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 function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class InterceptedHttpClient implements DelegateHttpClient
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var DelegateHttpClient */
|
||||
private $httpClient;
|
||||
/** @var ApplicationInterceptor */
|
||||
private $interceptor;
|
||||
public function __construct(DelegateHttpClient $httpClient, ApplicationInterceptor $interceptor)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
$this->interceptor = $interceptor;
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation) {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startRequest($request));
|
||||
}
|
||||
return $this->interceptor->request($request, $cancellation, $this->httpClient);
|
||||
});
|
||||
}
|
||||
}
|
15
dependencies/amphp/http-client/src/Interceptor/AddRequestHeader.php
vendored
Normal file
15
dependencies/amphp/http-client/src/Interceptor/AddRequestHeader.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
final class AddRequestHeader extends ModifyRequest
|
||||
{
|
||||
public function __construct(string $headerName, string ...$headerValues)
|
||||
{
|
||||
parent::__construct(static function (Request $request) use($headerName, $headerValues) {
|
||||
$request->addHeader($headerName, $headerValues);
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
}
|
15
dependencies/amphp/http-client/src/Interceptor/AddResponseHeader.php
vendored
Normal file
15
dependencies/amphp/http-client/src/Interceptor/AddResponseHeader.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
final class AddResponseHeader extends ModifyResponse
|
||||
{
|
||||
public function __construct(string $headerName, string ...$headerValues)
|
||||
{
|
||||
parent::__construct(static function (Response $response) use($headerName, $headerValues) {
|
||||
$response->addHeader($headerName, $headerValues);
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
}
|
76
dependencies/amphp/http-client/src/Interceptor/DecompressResponse.php
vendored
Normal file
76
dependencies/amphp/http-client/src/Interceptor/DecompressResponse.php
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\ZlibInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
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\Internal\SizeLimitingInputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\NetworkInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class DecompressResponse implements NetworkInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var bool */
|
||||
private $hasZlib;
|
||||
public function __construct()
|
||||
{
|
||||
$this->hasZlib = \extension_loaded('zlib');
|
||||
}
|
||||
public function requestViaNetwork(Request $request, CancellationToken $cancellation, Stream $stream) : Promise
|
||||
{
|
||||
// If a header is manually set, we won't interfere
|
||||
if ($request->hasHeader('accept-encoding')) {
|
||||
return $stream->request($request, $cancellation);
|
||||
}
|
||||
return call(function () use($request, $cancellation, $stream) {
|
||||
$this->addAcceptEncodingHeader($request);
|
||||
$request->interceptPush(function (Response $response) {
|
||||
return $this->decompressResponse($response);
|
||||
});
|
||||
return $this->decompressResponse((yield $stream->request($request, $cancellation)));
|
||||
});
|
||||
}
|
||||
private function addAcceptEncodingHeader(Request $request) : void
|
||||
{
|
||||
if ($this->hasZlib) {
|
||||
$request->setHeader('Accept-Encoding', 'gzip, deflate, identity');
|
||||
}
|
||||
}
|
||||
private function decompressResponse(Response $response) : Response
|
||||
{
|
||||
if ($encoding = $this->determineCompressionEncoding($response)) {
|
||||
$sizeLimit = $response->getRequest()->getBodySizeLimit();
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$decompressedBody = new ZlibInputStream($response->getBody(), $encoding);
|
||||
$response->setBody(new SizeLimitingInputStream($decompressedBody, $sizeLimit));
|
||||
$response->removeHeader('content-encoding');
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
private function determineCompressionEncoding(Response $response) : int
|
||||
{
|
||||
if (!$this->hasZlib) {
|
||||
return 0;
|
||||
}
|
||||
if (!$response->hasHeader("content-encoding")) {
|
||||
return 0;
|
||||
}
|
||||
$contentEncoding = $response->getHeader("content-encoding");
|
||||
\assert($contentEncoding !== null);
|
||||
$contentEncodingHeader = \trim($contentEncoding);
|
||||
if (\strcasecmp($contentEncodingHeader, 'gzip') === 0) {
|
||||
return \ZLIB_ENCODING_GZIP;
|
||||
}
|
||||
if (\strcasecmp($contentEncodingHeader, 'deflate') === 0) {
|
||||
return \ZLIB_ENCODING_DEFLATE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
245
dependencies/amphp/http-client/src/Interceptor/FollowRedirects.php
vendored
Normal file
245
dependencies/amphp/http-client/src/Interceptor/FollowRedirects.php
vendored
Normal file
@ -0,0 +1,245 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\StreamException;
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ApplicationInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
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\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\League\Uri;
|
||||
use WP_Ultimo\Dependencies\Psr\Http\Message\UriInterface as PsrUri;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class FollowRedirects implements ApplicationInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/**
|
||||
* Resolves the given path in $locationUri using $baseUri as a base URI. For example, a base URI of
|
||||
* http://example.com/example/path and a location path of 'to/resolve' will return a URI of
|
||||
* http://example.com/example/to/resolve.
|
||||
*
|
||||
* @param PsrUri $baseUri
|
||||
* @param PsrUri $locationUri
|
||||
*
|
||||
* @return PsrUri
|
||||
*/
|
||||
public static function resolve(PsrUri $baseUri, PsrUri $locationUri) : PsrUri
|
||||
{
|
||||
if ((string) $locationUri === '') {
|
||||
return $baseUri;
|
||||
}
|
||||
if ($locationUri->getScheme() !== '' || $locationUri->getHost() !== '') {
|
||||
$resultUri = $locationUri->withPath(self::removeDotSegments($locationUri->getPath()));
|
||||
if ($locationUri->getScheme() === '') {
|
||||
$resultUri = $resultUri->withScheme($baseUri->getScheme());
|
||||
}
|
||||
return $resultUri;
|
||||
}
|
||||
$baseUri = $baseUri->withQuery($locationUri->getQuery());
|
||||
$baseUri = $baseUri->withFragment($locationUri->getFragment());
|
||||
if ($locationUri->getPath() !== '' && \substr($locationUri->getPath(), 0, 1) === "/") {
|
||||
$baseUri = $baseUri->withPath(self::removeDotSegments($locationUri->getPath()));
|
||||
} else {
|
||||
$baseUri = $baseUri->withPath(self::mergePaths($baseUri->getPath(), $locationUri->getPath()));
|
||||
}
|
||||
return $baseUri;
|
||||
}
|
||||
/**
|
||||
* @param string $input
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @link http://www.apps.ietf.org/rfc/rfc3986.html#sec-5.2.4
|
||||
*/
|
||||
private static function removeDotSegments(string $input) : string
|
||||
{
|
||||
$output = '';
|
||||
$patternA = ',^(\\.\\.?/),';
|
||||
$patternB1 = ',^(/\\./),';
|
||||
$patternB2 = ',^(/\\.)$,';
|
||||
$patternC = ',^(/\\.\\./|/\\.\\.),';
|
||||
// $patternD = ',^(\.\.?)$,';
|
||||
$patternE = ',(/*[^/]*),';
|
||||
while ($input !== '') {
|
||||
if (\preg_match($patternA, $input)) {
|
||||
$input = \preg_replace($patternA, '', $input);
|
||||
} elseif (\preg_match($patternB1, $input, $match) || \preg_match($patternB2, $input, $match)) {
|
||||
$input = \preg_replace(",^" . $match[1] . ",", '/', $input);
|
||||
} elseif (\preg_match($patternC, $input, $match)) {
|
||||
$input = \preg_replace(',^' . \preg_quote($match[1], ',') . ',', '/', $input);
|
||||
$output = \preg_replace(',/([^/]+)$,', '', $output);
|
||||
} elseif ($input === '.' || $input === '..') {
|
||||
// pattern D
|
||||
$input = '';
|
||||
} elseif (\preg_match($patternE, $input, $match)) {
|
||||
$initialSegment = $match[1];
|
||||
$input = \preg_replace(',^' . \preg_quote($initialSegment, ',') . ',', '', $input, 1);
|
||||
$output .= $initialSegment;
|
||||
}
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
/**
|
||||
* @param string $basePath
|
||||
* @param string $pathToMerge
|
||||
*
|
||||
* @return string
|
||||
*
|
||||
* @link http://tools.ietf.org/html/rfc3986#section-5.2.3
|
||||
*/
|
||||
private static function mergePaths(string $basePath, string $pathToMerge) : string
|
||||
{
|
||||
if ($pathToMerge === '') {
|
||||
return self::removeDotSegments($basePath);
|
||||
}
|
||||
if ($basePath === '') {
|
||||
return self::removeDotSegments('/' . $pathToMerge);
|
||||
}
|
||||
$parts = \explode('/', $basePath);
|
||||
\array_pop($parts);
|
||||
$parts[] = $pathToMerge;
|
||||
return self::removeDotSegments(\implode('/', $parts));
|
||||
}
|
||||
/** @var int */
|
||||
private $maxRedirects;
|
||||
/** @var bool */
|
||||
private $autoReferrer;
|
||||
public function __construct(int $limit, bool $autoReferrer = \true)
|
||||
{
|
||||
if ($limit < 1) {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
throw new \Error("Invalid redirection limit: " . $limit);
|
||||
}
|
||||
$this->maxRedirects = $limit;
|
||||
$this->autoReferrer = $autoReferrer;
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise
|
||||
{
|
||||
// Don't follow redirects on pushes, just store the redirect in cache (if an interceptor is configured)
|
||||
return call(function () use($request, $cancellation, $httpClient) {
|
||||
/** @var Response $response */
|
||||
$response = (yield $httpClient->request(clone $request, $cancellation));
|
||||
$response = (yield from $this->followRedirects($request, $response, $httpClient, $cancellation));
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
private function followRedirects(Request $request, Response $response, DelegateHttpClient $client, CancellationToken $cancellationToken) : \Generator
|
||||
{
|
||||
$previousResponse = null;
|
||||
$maxRedirects = $this->maxRedirects;
|
||||
$requestNr = 2;
|
||||
do {
|
||||
$request = (yield from $this->createRedirectRequest($request, $response));
|
||||
if ($request === null) {
|
||||
return $response;
|
||||
}
|
||||
/** @var Response $redirectResponse */
|
||||
$redirectResponse = (yield $client->request(clone $request, $cancellationToken));
|
||||
$redirectResponse->setPreviousResponse($response);
|
||||
$response = $redirectResponse;
|
||||
} while (++$requestNr <= $maxRedirects + 1);
|
||||
if ($this->getRedirectUri($response) !== null) {
|
||||
throw new TooManyRedirectsException($response);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
private function createRedirectRequest(Request $originalRequest, Response $response) : \Generator
|
||||
{
|
||||
$redirectUri = $this->getRedirectUri($response);
|
||||
if ($redirectUri === null) {
|
||||
return null;
|
||||
}
|
||||
$originalUri = $response->getRequest()->getUri();
|
||||
$isSameHost = $redirectUri->getAuthority() === $originalUri->getAuthority();
|
||||
$request = clone $originalRequest;
|
||||
$request->setMethod('GET');
|
||||
$request->setUri($redirectUri);
|
||||
$request->removeHeader('transfer-encoding');
|
||||
$request->removeHeader('content-length');
|
||||
$request->removeHeader('content-type');
|
||||
$request->removeAttributes();
|
||||
$request->setBody(null);
|
||||
if (!$isSameHost) {
|
||||
// Remove for security reasons, any interceptor headers will be added again,
|
||||
// but application headers will be discarded.
|
||||
foreach ($request->getRawHeaders() as [$field]) {
|
||||
$request->removeHeader($field);
|
||||
}
|
||||
}
|
||||
if ($this->autoReferrer) {
|
||||
$this->assignRedirectRefererHeader($request, $originalUri, $redirectUri);
|
||||
}
|
||||
yield from $this->discardResponseBody($response);
|
||||
return $request;
|
||||
}
|
||||
/**
|
||||
* Clients must not add a Referer header when leaving an unencrypted resource and redirecting to an encrypted
|
||||
* resource.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param PsrUri $referrerUri
|
||||
* @param PsrUri $followUri
|
||||
*
|
||||
* @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3
|
||||
*/
|
||||
private function assignRedirectRefererHeader(Request $request, PsrUri $referrerUri, PsrUri $followUri) : void
|
||||
{
|
||||
$referrerIsEncrypted = $referrerUri->getScheme() === 'https';
|
||||
$destinationIsEncrypted = $followUri->getScheme() === 'https';
|
||||
if (!$referrerIsEncrypted || $destinationIsEncrypted) {
|
||||
$request->setHeader('Referer', (string) $referrerUri->withUserInfo('')->withFragment(''));
|
||||
} else {
|
||||
$request->removeHeader('Referer');
|
||||
}
|
||||
}
|
||||
private function getRedirectUri(Response $response) : ?PsrUri
|
||||
{
|
||||
if (\count($response->getHeaderArray('location')) !== 1) {
|
||||
return null;
|
||||
}
|
||||
$status = $response->getStatus();
|
||||
$request = $response->getRequest();
|
||||
$method = $request->getMethod();
|
||||
if ($method !== 'GET' && \in_array($status, [307, 308], \true)) {
|
||||
return null;
|
||||
}
|
||||
// We don't automatically follow:
|
||||
// - 300 (Multiple Choices)
|
||||
// - 304 (Not Modified)
|
||||
// - 305 (Use Proxy)
|
||||
if (!\in_array($status, [301, 302, 303, 307, 308], \true)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$location = $response->getHeader('location');
|
||||
\assert($location !== null);
|
||||
$locationUri = Uri\Http::createFromString($location);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
return self::resolve($request->getUri(), $locationUri);
|
||||
}
|
||||
private function discardResponseBody(Response $response) : \Generator
|
||||
{
|
||||
// Discard response body of redirect responses
|
||||
$body = $response->getBody();
|
||||
try {
|
||||
/** @noinspection PhpStatementHasEmptyBodyInspection */
|
||||
/** @noinspection LoopWhichDoesNotLoopInspection */
|
||||
/** @noinspection MissingOrEmptyGroupStatementInspection */
|
||||
while (null !== (yield $body->read())) {
|
||||
// discard
|
||||
}
|
||||
} catch (HttpException|StreamException $e) {
|
||||
// ignore streaming errors on previous responses
|
||||
} finally {
|
||||
unset($body);
|
||||
}
|
||||
}
|
||||
}
|
17
dependencies/amphp/http-client/src/Interceptor/ForbidUriUserInfo.php
vendored
Normal file
17
dependencies/amphp/http-client/src/Interceptor/ForbidUriUserInfo.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\InvalidRequestException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
final class ForbidUriUserInfo extends ModifyRequest
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct(static function (Request $request) {
|
||||
if ($request->getUri()->getUserInfo() !== '') {
|
||||
throw new InvalidRequestException($request, 'The user information (username:password) component of URIs has been deprecated ' . '(see https://tools.ietf.org/html/rfc3986#section-3.2.1 and https://tools.ietf.org/html/rfc7230#section-2.7.1); ' . 'Instead, set an "Authorization" header containing "Basic " . \\base64_encode("username:password"). ' . 'If you used HttpClientBuilder, you can use HttpClientBuilder::allowDeprecatedUriUserInfo() to disable this protection. ' . 'Doing so is strongly discouraged and you need to be aware of any interceptor using UriInterface::__toString(), which might expose the password in headers or logs.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
148
dependencies/amphp/http-client/src/Interceptor/LogHttpArchive.php
vendored
Normal file
148
dependencies/amphp/http-client/src/Interceptor/LogHttpArchive.php
vendored
Normal file
@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\File;
|
||||
use WP_Ultimo\Dependencies\Amp\File\Driver;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ApplicationInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\EventListener;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\EventListener\RecordHarAttributes;
|
||||
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\HarAttributes;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Message;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Sync\LocalMutex;
|
||||
use WP_Ultimo\Dependencies\Amp\Sync\Lock;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
use function WP_Ultimo\Dependencies\Amp\Promise\rethrow;
|
||||
final class LogHttpArchive implements ApplicationInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
private static function getTime(Request $request, string $start, string ...$ends) : int
|
||||
{
|
||||
if (!$request->hasAttribute($start)) {
|
||||
return -1;
|
||||
}
|
||||
foreach ($ends as $end) {
|
||||
if ($request->hasAttribute($end)) {
|
||||
return $request->getAttribute($end) - $request->getAttribute($start);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
private static function formatHeaders(Message $message) : array
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($message->getHeaders() as $field => $values) {
|
||||
foreach ($values as $value) {
|
||||
$headers[] = ['name' => $field, 'value' => $value];
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
private static function formatEntry(Response $response) : array
|
||||
{
|
||||
$request = $response->getRequest();
|
||||
$data = ['startedDateTime' => $request->getAttribute(HarAttributes::STARTED_DATE_TIME)->format(\DateTimeInterface::RFC3339_EXTENDED), 'time' => self::getTime($request, HarAttributes::TIME_START, HarAttributes::TIME_COMPLETE), 'request' => ['method' => $request->getMethod(), 'url' => (string) $request->getUri()->withUserInfo(''), 'httpVersion' => 'http/' . $request->getProtocolVersions()[0], 'headers' => self::formatHeaders($request), 'queryString' => [], 'cookies' => [], 'headersSize' => -1, 'bodySize' => -1], 'response' => ['status' => $response->getStatus(), 'statusText' => $response->getReason(), 'httpVersion' => 'http/' . $response->getProtocolVersion(), 'headers' => self::formatHeaders($response), 'cookies' => [], 'redirectURL' => $response->getHeader('location') ?? '', 'headersSize' => -1, 'bodySize' => -1, 'content' => ['size' => (int) ($response->getHeader('content-length') ?? '-1'), 'mimeType' => $response->getHeader('content-type') ?? '']], 'cache' => [], 'timings' => ['blocked' => self::getTime($request, HarAttributes::TIME_START, HarAttributes::TIME_CONNECT, HarAttributes::TIME_SEND), 'dns' => -1, 'connect' => self::getTime($request, HarAttributes::TIME_CONNECT, HarAttributes::TIME_SEND), 'ssl' => self::getTime($request, HarAttributes::TIME_SSL, HarAttributes::TIME_SEND), 'send' => self::getTime($request, HarAttributes::TIME_SEND, HarAttributes::TIME_WAIT), 'wait' => self::getTime($request, HarAttributes::TIME_WAIT, HarAttributes::TIME_RECEIVE), 'receive' => self::getTime($request, HarAttributes::TIME_RECEIVE, HarAttributes::TIME_COMPLETE)]];
|
||||
if ($request->hasAttribute(HarAttributes::SERVER_IP_ADDRESS)) {
|
||||
$data['serverIPAddress'] = $request->getAttribute(HarAttributes::SERVER_IP_ADDRESS);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
/** @var LocalMutex */
|
||||
private $fileMutex;
|
||||
/** @var File\File|null */
|
||||
private $fileHandle;
|
||||
/** @var string */
|
||||
private $filePath;
|
||||
/** @var \Throwable|null */
|
||||
private $error;
|
||||
/** @var EventListener */
|
||||
private $eventListener;
|
||||
public function __construct(string $filePath)
|
||||
{
|
||||
$this->filePath = $filePath;
|
||||
$this->fileMutex = new LocalMutex();
|
||||
$this->eventListener = new RecordHarAttributes();
|
||||
if (!\interface_exists(Driver::class)) {
|
||||
throw new \Error(__CLASS__ . ' requires amphp/file to be installed');
|
||||
}
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $httpClient) {
|
||||
if ($this->error) {
|
||||
throw $this->error;
|
||||
}
|
||||
$this->ensureEventListenerIsRegistered($request);
|
||||
/** @var Response $response */
|
||||
$response = (yield $httpClient->request($request, $cancellation));
|
||||
rethrow($this->writeLog($response));
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
public function reset() : Promise
|
||||
{
|
||||
return $this->rotate($this->filePath);
|
||||
}
|
||||
public function rotate(string $filePath) : Promise
|
||||
{
|
||||
return call(function () use($filePath) {
|
||||
/** @var Lock $lock */
|
||||
$lock = (yield $this->fileMutex->acquire());
|
||||
// Will automatically reopen and reset the file
|
||||
$this->fileHandle = null;
|
||||
$this->filePath = $filePath;
|
||||
$this->error = null;
|
||||
$lock->release();
|
||||
});
|
||||
}
|
||||
private function writeLog(Response $response) : Promise
|
||||
{
|
||||
return call(function () use($response) {
|
||||
try {
|
||||
(yield $response->getTrailers());
|
||||
} catch (\Throwable $e) {
|
||||
// ignore, still log the remaining response times
|
||||
}
|
||||
try {
|
||||
/** @var Lock $lock */
|
||||
$lock = (yield $this->fileMutex->acquire());
|
||||
$firstEntry = $this->fileHandle === null;
|
||||
if ($firstEntry) {
|
||||
$this->fileHandle = (yield \function_exists('WP_Ultimo\\Dependencies\\Amp\\File\\openFile') ? File\openFile($this->filePath, 'w') : File\open($this->filePath, 'w'));
|
||||
$header = '{"log":{"version":"1.2","creator":{"name":"amphp/http-client","version":"4.x"},"pages":[],"entries":[';
|
||||
(yield $this->fileHandle->write($header));
|
||||
} else {
|
||||
\assert($this->fileHandle !== null);
|
||||
(yield $this->fileHandle->seek(-3, \SEEK_CUR));
|
||||
}
|
||||
/** @noinspection PhpComposerExtensionStubsInspection */
|
||||
$json = \json_encode(self::formatEntry($response));
|
||||
(yield $this->fileHandle->write(($firstEntry ? '' : ',') . $json . ']}}'));
|
||||
$lock->release();
|
||||
} catch (HttpException $e) {
|
||||
$this->error = $e;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error = new HttpException('Writing HTTP archive log failed', 0, $e);
|
||||
}
|
||||
});
|
||||
}
|
||||
private function ensureEventListenerIsRegistered(Request $request) : void
|
||||
{
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
if ($eventListener instanceof RecordHarAttributes) {
|
||||
return;
|
||||
// user added it manually
|
||||
}
|
||||
}
|
||||
$request->addEventListener($this->eventListener);
|
||||
}
|
||||
}
|
80
dependencies/amphp/http-client/src/Interceptor/MatchOrigin.php
vendored
Normal file
80
dependencies/amphp/http-client/src/Interceptor/MatchOrigin.php
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ApplicationInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
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\Request;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\League\Uri\Http;
|
||||
use WP_Ultimo\Dependencies\Psr\Http\Message\UriInterface;
|
||||
final class MatchOrigin implements ApplicationInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var ApplicationInterceptor[] */
|
||||
private $originMap = [];
|
||||
/** @var ApplicationInterceptor|null */
|
||||
private $default;
|
||||
/**
|
||||
* @param ApplicationInterceptor[] $originMap
|
||||
* @param ApplicationInterceptor $default
|
||||
*
|
||||
* @throws HttpException
|
||||
*/
|
||||
public function __construct(array $originMap, ?ApplicationInterceptor $default = null)
|
||||
{
|
||||
foreach ($originMap as $origin => $interceptor) {
|
||||
if (!$interceptor instanceof ApplicationInterceptor) {
|
||||
$type = \is_object($interceptor) ? \get_class($interceptor) : \gettype($interceptor);
|
||||
throw new HttpException('Origin map must be a map from origin to ApplicationInterceptor, got ' . $type);
|
||||
}
|
||||
$this->originMap[$this->checkOrigin($origin)] = $interceptor;
|
||||
}
|
||||
$this->default = $default;
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise
|
||||
{
|
||||
$interceptor = $this->originMap[$this->normalizeOrigin($request->getUri())] ?? $this->default;
|
||||
if (!$interceptor) {
|
||||
return $httpClient->request($request, $cancellation);
|
||||
}
|
||||
return $interceptor->request($request, $cancellation, $httpClient);
|
||||
}
|
||||
private function checkOrigin(string $origin) : string
|
||||
{
|
||||
try {
|
||||
$originUri = Http::createFromString($origin);
|
||||
} catch (\Exception $e) {
|
||||
throw new HttpException("Invalid origin provided: parsing failed: " . $origin);
|
||||
}
|
||||
if (!\in_array($originUri->getScheme(), ['http', 'https'], \true)) {
|
||||
throw new HttpException('Invalid origin with unsupported scheme: ' . $origin);
|
||||
}
|
||||
if ($originUri->getHost() === '') {
|
||||
throw new HttpException('Invalid origin without host: ' . $origin);
|
||||
}
|
||||
if ($originUri->getUserInfo() !== '') {
|
||||
throw new HttpException('Invalid origin with user info, which must not be present: ' . $origin);
|
||||
}
|
||||
if (!\in_array($originUri->getPath(), ['', '/'], \true)) {
|
||||
throw new HttpException('Invalid origin with path, which must not be present: ' . $origin);
|
||||
}
|
||||
if ($originUri->getQuery() !== '') {
|
||||
throw new HttpException('Invalid origin with query, which must not be present: ' . $origin);
|
||||
}
|
||||
if ($originUri->getFragment() !== '') {
|
||||
throw new HttpException('Invalid origin with fragment, which must not be present: ' . $origin);
|
||||
}
|
||||
return $this->normalizeOrigin($originUri);
|
||||
}
|
||||
private function normalizeOrigin(UriInterface $uri) : string
|
||||
{
|
||||
$defaultPort = $uri->getScheme() === 'https' ? 443 : 80;
|
||||
return $uri->getScheme() . '://' . $uri->getHost() . ':' . ($uri->getPort() ?? $defaultPort);
|
||||
}
|
||||
}
|
52
dependencies/amphp/http-client/src/Interceptor/ModifyRequest.php
vendored
Normal file
52
dependencies/amphp/http-client/src/Interceptor/ModifyRequest.php
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ApplicationInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
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\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
class ModifyRequest implements NetworkInterceptor, ApplicationInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var callable(Request):(\Generator<mixed, mixed, mixed, Promise<Request|null>|Request|null>|Promise<Request|null>|Request|null) */
|
||||
private $mapper;
|
||||
/**
|
||||
* @psalm-param callable(Request):(\Generator<mixed, mixed, mixed, Promise<Request|null>|Request|null>|Promise<Request|null>|Request|null) $mapper
|
||||
*/
|
||||
public function __construct(callable $mapper)
|
||||
{
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return Promise<Response>
|
||||
*/
|
||||
public final function requestViaNetwork(Request $request, CancellationToken $cancellation, Stream $stream) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $stream) {
|
||||
$mappedRequest = (yield call($this->mapper, $request));
|
||||
\assert($mappedRequest instanceof Request || $mappedRequest === null);
|
||||
return (yield $stream->request($mappedRequest ?? $request, $cancellation));
|
||||
});
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $httpClient) {
|
||||
$mappedRequest = (yield call($this->mapper, $request));
|
||||
\assert($mappedRequest instanceof Request || $mappedRequest === null);
|
||||
return $httpClient->request($mappedRequest ?? $request, $cancellation);
|
||||
});
|
||||
}
|
||||
}
|
48
dependencies/amphp/http-client/src/Interceptor/ModifyResponse.php
vendored
Normal file
48
dependencies/amphp/http-client/src/Interceptor/ModifyResponse.php
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ApplicationInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
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\Http\Client\Response;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
class ModifyResponse implements NetworkInterceptor, ApplicationInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var callable(Response):(\Generator<mixed, mixed, mixed, Response|null>|Promise<Response>|Response|null) */
|
||||
private $mapper;
|
||||
/**
|
||||
* @psalm-param callable(Response):(\Generator<mixed, mixed, mixed, Response|null>|Promise<Response>|Response|null) $mapper
|
||||
*/
|
||||
public function __construct(callable $mapper)
|
||||
{
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
public final function requestViaNetwork(Request $request, CancellationToken $cancellation, Stream $stream) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $stream) {
|
||||
$response = (yield $stream->request($request, $cancellation));
|
||||
$mappedResponse = (yield call($this->mapper, $response));
|
||||
\assert($mappedResponse instanceof Response || $mappedResponse === null);
|
||||
return $mappedResponse ?? $response;
|
||||
});
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $httpClient) {
|
||||
$request->interceptPush($this->mapper);
|
||||
$response = (yield $httpClient->request($request, $cancellation));
|
||||
$mappedResponse = (yield call($this->mapper, $response));
|
||||
\assert($mappedResponse instanceof Response || $mappedResponse === null);
|
||||
return $mappedResponse ?? $response;
|
||||
});
|
||||
}
|
||||
}
|
15
dependencies/amphp/http-client/src/Interceptor/RemoveRequestHeader.php
vendored
Normal file
15
dependencies/amphp/http-client/src/Interceptor/RemoveRequestHeader.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
final class RemoveRequestHeader extends ModifyRequest
|
||||
{
|
||||
public function __construct(string $headerName)
|
||||
{
|
||||
parent::__construct(static function (Request $request) use($headerName) {
|
||||
$request->removeHeader($headerName);
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
}
|
15
dependencies/amphp/http-client/src/Interceptor/RemoveResponseHeader.php
vendored
Normal file
15
dependencies/amphp/http-client/src/Interceptor/RemoveResponseHeader.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
final class RemoveResponseHeader extends ModifyResponse
|
||||
{
|
||||
public function __construct(string $headerName)
|
||||
{
|
||||
parent::__construct(static function (Response $response) use($headerName) {
|
||||
$response->removeHeader($headerName);
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
}
|
45
dependencies/amphp/http-client/src/Interceptor/RetryRequests.php
vendored
Normal file
45
dependencies/amphp/http-client/src/Interceptor/RetryRequests.php
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ApplicationInterceptor;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Http2ConnectionException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\UnprocessedRequestException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\DelegateHttpClient;
|
||||
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\SocketException;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class RetryRequests implements ApplicationInterceptor
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var int */
|
||||
private $retryLimit;
|
||||
public function __construct(int $retryLimit)
|
||||
{
|
||||
$this->retryLimit = $retryLimit;
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation, DelegateHttpClient $httpClient) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation, $httpClient) {
|
||||
$attempt = 1;
|
||||
do {
|
||||
try {
|
||||
return (yield $httpClient->request(clone $request, $cancellation));
|
||||
} catch (UnprocessedRequestException $exception) {
|
||||
// Request was deemed retryable by connection, so carry on.
|
||||
} catch (SocketException|Http2ConnectionException $exception) {
|
||||
if (!$request->isIdempotent()) {
|
||||
throw $exception;
|
||||
}
|
||||
// Request can safely be retried.
|
||||
}
|
||||
} while ($attempt++ <= $this->retryLimit);
|
||||
throw $exception;
|
||||
});
|
||||
}
|
||||
}
|
16
dependencies/amphp/http-client/src/Interceptor/SetRequestHeader.php
vendored
Normal file
16
dependencies/amphp/http-client/src/Interceptor/SetRequestHeader.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
final class SetRequestHeader extends ModifyRequest
|
||||
{
|
||||
public function __construct(string $headerName, string $headerValue, string ...$headerValues)
|
||||
{
|
||||
\array_unshift($headerValues, $headerValue);
|
||||
parent::__construct(static function (Request $request) use($headerName, $headerValues) {
|
||||
$request->setHeader($headerName, $headerValues);
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
}
|
18
dependencies/amphp/http-client/src/Interceptor/SetRequestHeaderIfUnset.php
vendored
Normal file
18
dependencies/amphp/http-client/src/Interceptor/SetRequestHeaderIfUnset.php
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
final class SetRequestHeaderIfUnset extends ModifyRequest
|
||||
{
|
||||
public function __construct(string $headerName, string $headerValue, string ...$headerValues)
|
||||
{
|
||||
\array_unshift($headerValues, $headerValue);
|
||||
parent::__construct(static function (Request $request) use($headerName, $headerValues) {
|
||||
if (!$request->hasHeader($headerName)) {
|
||||
$request->setHeader($headerName, $headerValues);
|
||||
}
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
}
|
17
dependencies/amphp/http-client/src/Interceptor/SetRequestTimeout.php
vendored
Normal file
17
dependencies/amphp/http-client/src/Interceptor/SetRequestTimeout.php
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
final class SetRequestTimeout extends ModifyRequest
|
||||
{
|
||||
public function __construct(int $tcpConnectTimeout = 10000, int $tlsHandshakeTimeout = 10000, int $transferTimeout = 10000)
|
||||
{
|
||||
parent::__construct(static function (Request $request) use($tcpConnectTimeout, $tlsHandshakeTimeout, $transferTimeout) {
|
||||
$request->setTcpConnectTimeout($tcpConnectTimeout);
|
||||
$request->setTlsHandshakeTimeout($tlsHandshakeTimeout);
|
||||
$request->setTransferTimeout($transferTimeout);
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
}
|
16
dependencies/amphp/http-client/src/Interceptor/SetResponseHeader.php
vendored
Normal file
16
dependencies/amphp/http-client/src/Interceptor/SetResponseHeader.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
final class SetResponseHeader extends ModifyResponse
|
||||
{
|
||||
public function __construct(string $headerName, string $headerValue, string ...$headerValues)
|
||||
{
|
||||
\array_unshift($headerValues, $headerValue);
|
||||
parent::__construct(static function (Response $response) use($headerName, $headerValues) {
|
||||
$response->setHeader($headerName, $headerValues);
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
}
|
18
dependencies/amphp/http-client/src/Interceptor/SetResponseHeaderIfUnset.php
vendored
Normal file
18
dependencies/amphp/http-client/src/Interceptor/SetResponseHeaderIfUnset.php
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
final class SetResponseHeaderIfUnset extends ModifyResponse
|
||||
{
|
||||
public function __construct(string $headerName, string $headerValue, string ...$headerValues)
|
||||
{
|
||||
\array_unshift($headerValues, $headerValue);
|
||||
parent::__construct(static function (Response $response) use($headerName, $headerValues) {
|
||||
if (!$response->hasHeader($headerName)) {
|
||||
$response->setHeader($headerName, $headerValues);
|
||||
}
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
}
|
20
dependencies/amphp/http-client/src/Interceptor/TooManyRedirectsException.php
vendored
Normal file
20
dependencies/amphp/http-client/src/Interceptor/TooManyRedirectsException.php
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Interceptor;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\HttpException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Response;
|
||||
class TooManyRedirectsException extends HttpException
|
||||
{
|
||||
/** @var Response */
|
||||
private $response;
|
||||
public function __construct(Response $response)
|
||||
{
|
||||
parent::__construct("There were too many redirects");
|
||||
$this->response = $response;
|
||||
}
|
||||
public function getResponse() : Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
13
dependencies/amphp/http-client/src/Internal/ForbidCloning.php
vendored
Normal file
13
dependencies/amphp/http-client/src/Internal/ForbidCloning.php
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Internal;
|
||||
|
||||
/** @internal */
|
||||
trait ForbidCloning
|
||||
{
|
||||
protected final function __clone()
|
||||
{
|
||||
// clone is automatically denied to all external calls
|
||||
// final protected instead of private to also force denial for all children
|
||||
}
|
||||
}
|
12
dependencies/amphp/http-client/src/Internal/ForbidSerialization.php
vendored
Normal file
12
dependencies/amphp/http-client/src/Internal/ForbidSerialization.php
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Internal;
|
||||
|
||||
/** @internal */
|
||||
trait ForbidSerialization
|
||||
{
|
||||
public final function __sleep()
|
||||
{
|
||||
throw new \Error(__CLASS__ . ' does not support serialization');
|
||||
}
|
||||
}
|
19
dependencies/amphp/http-client/src/Internal/HarAttributes.php
vendored
Normal file
19
dependencies/amphp/http-client/src/Internal/HarAttributes.php
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Internal;
|
||||
|
||||
/** @internal */
|
||||
final class HarAttributes
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
public const STARTED_DATE_TIME = 'amp.http.client.har.startedDateTime';
|
||||
public const SERVER_IP_ADDRESS = 'amp.http.client.har.serverIPAddress';
|
||||
public const TIME_START = 'amp.http.client.har.timings.start';
|
||||
public const TIME_SSL = 'amp.http.client.har.timings.ssl';
|
||||
public const TIME_CONNECT = 'amp.http.client.har.timings.connect';
|
||||
public const TIME_SEND = 'amp.http.client.har.timings.send';
|
||||
public const TIME_WAIT = 'amp.http.client.har.timings.wait';
|
||||
public const TIME_RECEIVE = 'amp.http.client.har.timings.receive';
|
||||
public const TIME_COMPLETE = 'amp.http.client.har.timings.complete';
|
||||
}
|
40
dependencies/amphp/http-client/src/Internal/ResponseBodyStream.php
vendored
Normal file
40
dependencies/amphp/http-client/src/Internal/ResponseBodyStream.php
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationTokenSource;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/** @internal */
|
||||
final class ResponseBodyStream implements InputStream
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
/** @var InputStream */
|
||||
private $body;
|
||||
/** @var CancellationTokenSource */
|
||||
private $bodyCancellation;
|
||||
/** @var bool */
|
||||
private $successfulEnd = \false;
|
||||
public function __construct(InputStream $body, CancellationTokenSource $bodyCancellation)
|
||||
{
|
||||
$this->body = $body;
|
||||
$this->bodyCancellation = $bodyCancellation;
|
||||
}
|
||||
public function read() : Promise
|
||||
{
|
||||
$promise = $this->body->read();
|
||||
$promise->onResolve(function ($error, $value) {
|
||||
if ($value === null && $error === null) {
|
||||
$this->successfulEnd = \true;
|
||||
}
|
||||
});
|
||||
return $promise;
|
||||
}
|
||||
public function __destruct()
|
||||
{
|
||||
if (!$this->successfulEnd) {
|
||||
$this->bodyCancellation->cancel();
|
||||
}
|
||||
}
|
||||
}
|
46
dependencies/amphp/http-client/src/Internal/SizeLimitingInputStream.php
vendored
Normal file
46
dependencies/amphp/http-client/src/Internal/SizeLimitingInputStream.php
vendored
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Failure;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\ParseException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Status;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/** @internal */
|
||||
final class SizeLimitingInputStream implements InputStream
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
/** @var InputStream|null */
|
||||
private $source;
|
||||
/** @var int */
|
||||
private $bytesRead = 0;
|
||||
/** @var int */
|
||||
private $sizeLimit;
|
||||
/** @var \Throwable|null */
|
||||
private $exception;
|
||||
public function __construct(InputStream $source, int $sizeLimit)
|
||||
{
|
||||
$this->source = $source;
|
||||
$this->sizeLimit = $sizeLimit;
|
||||
}
|
||||
public function read() : Promise
|
||||
{
|
||||
if ($this->exception) {
|
||||
return new Failure($this->exception);
|
||||
}
|
||||
\assert($this->source !== null);
|
||||
$promise = $this->source->read();
|
||||
$promise->onResolve(function ($error, $value) {
|
||||
if ($value !== null) {
|
||||
$this->bytesRead += \strlen($value);
|
||||
if ($this->bytesRead > $this->sizeLimit) {
|
||||
$this->exception = new ParseException("Configured body size exceeded: {$this->bytesRead} bytes received, while the configured limit is {$this->sizeLimit} bytes", Status::PAYLOAD_TOO_LARGE);
|
||||
$this->source = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
return $promise;
|
||||
}
|
||||
}
|
26
dependencies/amphp/http-client/src/Internal/functions.php
vendored
Normal file
26
dependencies/amphp/http-client/src/Internal/functions.php
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client\Internal;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\InvalidRequestException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Request;
|
||||
/**
|
||||
* @param Request $request
|
||||
*
|
||||
* @return string
|
||||
* @throws InvalidRequestException
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
function normalizeRequestPathWithQuery(Request $request) : string
|
||||
{
|
||||
$path = $request->getUri()->getPath();
|
||||
$query = $request->getUri()->getQuery();
|
||||
if ($path === '') {
|
||||
return '/' . ($query !== '' ? '?' . $query : '');
|
||||
}
|
||||
if ($path[0] !== '/') {
|
||||
throw new InvalidRequestException($request, 'Relative path (' . $path . ') is not allowed in the request URI');
|
||||
}
|
||||
return $path . ($query !== '' ? '?' . $query : '');
|
||||
}
|
18
dependencies/amphp/http-client/src/InvalidRequestException.php
vendored
Normal file
18
dependencies/amphp/http-client/src/InvalidRequestException.php
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
final class InvalidRequestException extends HttpException
|
||||
{
|
||||
/** @var Request */
|
||||
private $request;
|
||||
public function __construct(Request $request, string $message, int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->request = $request;
|
||||
}
|
||||
public function getRequest() : Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
11
dependencies/amphp/http-client/src/MissingAttributeError.php
vendored
Normal file
11
dependencies/amphp/http-client/src/MissingAttributeError.php
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
final class MissingAttributeError extends \Error
|
||||
{
|
||||
public function __construct(string $message)
|
||||
{
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
30
dependencies/amphp/http-client/src/NetworkInterceptor.php
vendored
Normal file
30
dependencies/amphp/http-client/src/NetworkInterceptor.php
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/**
|
||||
* Allows intercepting an HTTP request after the connection to the remote server has been established.
|
||||
*/
|
||||
interface NetworkInterceptor
|
||||
{
|
||||
/**
|
||||
* Intercepts an HTTP request after the connection to the remote server has been established.
|
||||
*
|
||||
* The implementation might modify the request and/or modify the response after the promise returned from
|
||||
* `$stream->request(...)` resolved.
|
||||
*
|
||||
* A NetworkInterceptor SHOULD NOT short-circuit and SHOULD delegate to the `$stream` passed as third argument
|
||||
* exactly once. The only exception to this is throwing an exception, e.g. because the TLS settings used are
|
||||
* unacceptable. If you need short circuits, use an {@see ApplicationInterceptor} instead.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param CancellationToken $cancellation
|
||||
* @param Stream $stream
|
||||
*
|
||||
* @return Promise<Response>
|
||||
*/
|
||||
public function requestViaNetwork(Request $request, CancellationToken $cancellation, Stream $stream) : Promise;
|
||||
}
|
16
dependencies/amphp/http-client/src/ParseException.php
vendored
Normal file
16
dependencies/amphp/http-client/src/ParseException.php
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
final class ParseException extends HttpException
|
||||
{
|
||||
/**
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @param \Throwable|null $previousException
|
||||
*/
|
||||
public function __construct(string $message, int $code, \Throwable $previousException = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previousException);
|
||||
}
|
||||
}
|
60
dependencies/amphp/http-client/src/PooledHttpClient.php
vendored
Normal file
60
dependencies/amphp/http-client/src/PooledHttpClient.php
vendored
Normal file
@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\CancellationToken;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\ConnectionPool;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\InterceptedStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\Stream;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Connection\UnlimitedConnectionPool;
|
||||
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 function WP_Ultimo\Dependencies\Amp\call;
|
||||
final class PooledHttpClient implements DelegateHttpClient
|
||||
{
|
||||
use ForbidCloning;
|
||||
use ForbidSerialization;
|
||||
/** @var ConnectionPool */
|
||||
private $connectionPool;
|
||||
/** @var NetworkInterceptor[] */
|
||||
private $networkInterceptors = [];
|
||||
public function __construct(?ConnectionPool $connectionPool = null)
|
||||
{
|
||||
$this->connectionPool = $connectionPool ?? new UnlimitedConnectionPool();
|
||||
}
|
||||
public function request(Request $request, CancellationToken $cancellation) : Promise
|
||||
{
|
||||
return call(function () use($request, $cancellation) {
|
||||
foreach ($request->getEventListeners() as $eventListener) {
|
||||
(yield $eventListener->startRequest($request));
|
||||
}
|
||||
$stream = (yield $this->connectionPool->getStream($request, $cancellation));
|
||||
\assert($stream instanceof Stream);
|
||||
foreach (\array_reverse($this->networkInterceptors) as $interceptor) {
|
||||
$stream = new InterceptedStream($stream, $interceptor);
|
||||
}
|
||||
return (yield $stream->request($request, $cancellation));
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Adds a network interceptor.
|
||||
*
|
||||
* Network interceptors are only invoked if the request requires network access, i.e. there's no short-circuit by
|
||||
* an application interceptor, e.g. a cache.
|
||||
*
|
||||
* Whether the given network interceptor will be respected for currently running requests is undefined.
|
||||
*
|
||||
* Any new requests have to take the new interceptor into account.
|
||||
*
|
||||
* @param NetworkInterceptor $networkInterceptor
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function intercept(NetworkInterceptor $networkInterceptor) : self
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->networkInterceptors[] = $networkInterceptor;
|
||||
return $clone;
|
||||
}
|
||||
}
|
468
dependencies/amphp/http-client/src/Request.php
vendored
Normal file
468
dependencies/amphp/http-client/src/Request.php
vendored
Normal file
@ -0,0 +1,468 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Body\StringBody;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Message;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\League\Uri;
|
||||
use WP_Ultimo\Dependencies\Psr\Http\Message\UriInterface;
|
||||
use function WP_Ultimo\Dependencies\Amp\call;
|
||||
/**
|
||||
* An HTTP request.
|
||||
*/
|
||||
final class Request extends Message
|
||||
{
|
||||
use ForbidSerialization;
|
||||
public const DEFAULT_HEADER_SIZE_LIMIT = 2 * 8192;
|
||||
public const DEFAULT_BODY_SIZE_LIMIT = 10485760;
|
||||
/**
|
||||
* @template TValue
|
||||
*
|
||||
* @param mixed $value
|
||||
* @psalm-param TValue $value
|
||||
*
|
||||
* @return mixed
|
||||
* @psalm-return TValue
|
||||
*/
|
||||
private static function clone($value)
|
||||
{
|
||||
if ($value === null || \is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
// force deep cloning
|
||||
return \unserialize(\serialize($value), ['allowed_classes' => \true]);
|
||||
}
|
||||
/** @var string[] */
|
||||
private $protocolVersions = ['1.1', '2'];
|
||||
/** @var string */
|
||||
private $method;
|
||||
/** @var UriInterface */
|
||||
private $uri;
|
||||
/** @var RequestBody */
|
||||
private $body;
|
||||
/** @var int */
|
||||
private $tcpConnectTimeout = 10000;
|
||||
/** @var int */
|
||||
private $tlsHandshakeTimeout = 10000;
|
||||
/** @var int */
|
||||
private $transferTimeout = 10000;
|
||||
/** @var int */
|
||||
private $inactivityTimeout = 10000;
|
||||
/** @var int */
|
||||
private $bodySizeLimit = self::DEFAULT_BODY_SIZE_LIMIT;
|
||||
/** @var int */
|
||||
private $headerSizeLimit = self::DEFAULT_HEADER_SIZE_LIMIT;
|
||||
/** @var callable|null */
|
||||
private $onPush;
|
||||
/** @var callable|null */
|
||||
private $onUpgrade;
|
||||
/** @var callable|null */
|
||||
private $onInformationalResponse;
|
||||
/** @var mixed[] */
|
||||
private $attributes = [];
|
||||
/** @var EventListener[] */
|
||||
private $eventListeners = [];
|
||||
/**
|
||||
* Request constructor.
|
||||
*
|
||||
* @param string|UriInterface $uri
|
||||
* @param string $method
|
||||
* @param string $body
|
||||
*/
|
||||
public function __construct($uri, string $method = "GET", ?string $body = null)
|
||||
{
|
||||
$this->setUri($uri);
|
||||
$this->setMethod($method);
|
||||
$this->setBody($body);
|
||||
}
|
||||
public function addEventListener(EventListener $eventListener) : void
|
||||
{
|
||||
$this->eventListeners[] = $eventListener;
|
||||
}
|
||||
/**
|
||||
* @return EventListener[]
|
||||
*/
|
||||
public function getEventListeners() : array
|
||||
{
|
||||
return $this->eventListeners;
|
||||
}
|
||||
/**
|
||||
* Retrieve the requests's acceptable HTTP protocol versions.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getProtocolVersions() : array
|
||||
{
|
||||
return $this->protocolVersions;
|
||||
}
|
||||
/**
|
||||
* Assign the requests's acceptable HTTP protocol versions.
|
||||
*
|
||||
* The HTTP client might choose any of these.
|
||||
*
|
||||
* @param string[] $versions
|
||||
*/
|
||||
public function setProtocolVersions(array $versions) : void
|
||||
{
|
||||
$versions = \array_unique($versions);
|
||||
if (empty($versions)) {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
throw new \Error("Empty array of protocol versions provided, must not be empty.");
|
||||
}
|
||||
foreach ($versions as $version) {
|
||||
if (!\in_array($version, ["1.0", "1.1", "2"], \true)) {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
throw new \Error("Invalid HTTP protocol version: " . $version);
|
||||
}
|
||||
}
|
||||
$this->protocolVersions = $versions;
|
||||
}
|
||||
/**
|
||||
* Retrieve the request's HTTP method verb.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMethod() : string
|
||||
{
|
||||
return $this->method;
|
||||
}
|
||||
/**
|
||||
* Specify the request's HTTP method verb.
|
||||
*
|
||||
* @param string $method
|
||||
*/
|
||||
public function setMethod(string $method) : void
|
||||
{
|
||||
$this->method = $method;
|
||||
}
|
||||
/**
|
||||
* Retrieve the request's URI.
|
||||
*
|
||||
* @return UriInterface
|
||||
*/
|
||||
public function getUri() : UriInterface
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
/**
|
||||
* Specify the request's HTTP URI.
|
||||
*
|
||||
* @param string|UriInterface $uri
|
||||
*/
|
||||
public function setUri($uri) : void
|
||||
{
|
||||
$this->uri = $uri instanceof UriInterface ? $uri : $this->createUriFromString($uri);
|
||||
}
|
||||
/**
|
||||
* Assign a value for the specified header field by replacing any existing values for that field.
|
||||
*
|
||||
* @param string $name Header name.
|
||||
* @param string|string[] $value Header value.
|
||||
*/
|
||||
public function setHeader(string $name, $value) : void
|
||||
{
|
||||
if (($name[0] ?? ":") === ":") {
|
||||
throw new \Error("Header name cannot be empty or start with a colon (:)");
|
||||
}
|
||||
parent::setHeader($name, $value);
|
||||
}
|
||||
/**
|
||||
* Assign a value for the specified header field by adding an additional header line.
|
||||
*
|
||||
* @param string $name Header name.
|
||||
* @param string|string[] $value Header value.
|
||||
*/
|
||||
public function addHeader(string $name, $value) : void
|
||||
{
|
||||
if (($name[0] ?? ":") === ":") {
|
||||
throw new \Error("Header name cannot be empty or start with a colon (:)");
|
||||
}
|
||||
parent::addHeader($name, $value);
|
||||
}
|
||||
public function setHeaders(array $headers) : void
|
||||
{
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
parent::setHeaders($headers);
|
||||
}
|
||||
/**
|
||||
* Remove the specified header field from the message.
|
||||
*
|
||||
* @param string $name Header name.
|
||||
*/
|
||||
public function removeHeader(string $name) : void
|
||||
{
|
||||
parent::removeHeader($name);
|
||||
}
|
||||
/**
|
||||
* Retrieve the message entity body.
|
||||
*/
|
||||
public function getBody() : RequestBody
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
/**
|
||||
* Assign the message entity body.
|
||||
*
|
||||
* @param mixed $body
|
||||
*/
|
||||
public function setBody($body) : void
|
||||
{
|
||||
if ($body === null) {
|
||||
$this->body = new StringBody("");
|
||||
} elseif (\is_string($body)) {
|
||||
$this->body = new StringBody($body);
|
||||
} elseif (\is_scalar($body)) {
|
||||
$this->body = new StringBody(\var_export($body, \true));
|
||||
} elseif ($body instanceof RequestBody) {
|
||||
$this->body = $body;
|
||||
} else {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
throw new \TypeError("Invalid body type: " . \gettype($body));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Registers a callback to the request that is invoked when the server pushes an additional resource.
|
||||
* The callback is given two parameters: the Request generated from the pushed resource, and a promise for the
|
||||
* Response containing the pushed resource. An HttpException, StreamException, or CancelledException can be thrown
|
||||
* to refuse the push. If no callback is registered, pushes are automatically rejected.
|
||||
*
|
||||
* Interceptors can mostly use {@code interceptPush} instead.
|
||||
*
|
||||
* Example:
|
||||
* function (Request $request, Promise $response): \Generator {
|
||||
* $uri = $request->getUri(); // URI of pushed resource.
|
||||
* $response = yield $promise; // Wait for resource to arrive.
|
||||
* // Use Response object from resolved promise.
|
||||
* }
|
||||
*
|
||||
* @param callable|null $onPush
|
||||
*/
|
||||
public function setPushHandler(?callable $onPush) : void
|
||||
{
|
||||
$this->onPush = $onPush;
|
||||
}
|
||||
/**
|
||||
* Allows interceptors to modify also pushed responses.
|
||||
*
|
||||
* If no push callable has been set by the application, the interceptor won't be invoked. If you want to enable
|
||||
* push in an interceptor without the application setting a push handler, you need to use {@code setPushHandler}.
|
||||
*
|
||||
* @param callable $interceptor Receives the response and might modify it or return a new instance.
|
||||
*/
|
||||
public function interceptPush(callable $interceptor) : void
|
||||
{
|
||||
if ($this->onPush === null) {
|
||||
return;
|
||||
}
|
||||
$onPush = $this->onPush;
|
||||
/** @psalm-suppress MissingClosureReturnType */
|
||||
$this->onPush = static function (Request $request, Promise $response) use($onPush, $interceptor) {
|
||||
$response = call(static function () use($response, $interceptor) : \Generator {
|
||||
return (yield call($interceptor, (yield $response))) ?? $response;
|
||||
});
|
||||
return $onPush($request, $response);
|
||||
};
|
||||
}
|
||||
/**
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getPushHandler() : ?callable
|
||||
{
|
||||
return $this->onPush;
|
||||
}
|
||||
/**
|
||||
* Registers a callback invoked if a 101 response is returned to the request.
|
||||
*
|
||||
* @param callable|null $onUpgrade
|
||||
*/
|
||||
public function setUpgradeHandler(?callable $onUpgrade) : void
|
||||
{
|
||||
$this->onUpgrade = $onUpgrade;
|
||||
}
|
||||
/**
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getUpgradeHandler() : ?callable
|
||||
{
|
||||
return $this->onUpgrade;
|
||||
}
|
||||
/**
|
||||
* Registers a callback invoked when a 1xx response is returned to the request (other than a 101).
|
||||
*
|
||||
* @param callable|null $onInformationalResponse
|
||||
*/
|
||||
public function setInformationalResponseHandler(?callable $onInformationalResponse) : void
|
||||
{
|
||||
$this->onInformationalResponse = $onInformationalResponse;
|
||||
}
|
||||
/**
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getInformationalResponseHandler() : ?callable
|
||||
{
|
||||
return $this->onInformationalResponse;
|
||||
}
|
||||
/**
|
||||
* @return int Timeout in milliseconds for the TCP connection.
|
||||
*/
|
||||
public function getTcpConnectTimeout() : int
|
||||
{
|
||||
return $this->tcpConnectTimeout;
|
||||
}
|
||||
public function setTcpConnectTimeout(int $tcpConnectTimeout) : void
|
||||
{
|
||||
$this->tcpConnectTimeout = $tcpConnectTimeout;
|
||||
}
|
||||
/**
|
||||
* @return int Timeout in milliseconds for the TLS handshake.
|
||||
*/
|
||||
public function getTlsHandshakeTimeout() : int
|
||||
{
|
||||
return $this->tlsHandshakeTimeout;
|
||||
}
|
||||
public function setTlsHandshakeTimeout(int $tlsHandshakeTimeout) : void
|
||||
{
|
||||
$this->tlsHandshakeTimeout = $tlsHandshakeTimeout;
|
||||
}
|
||||
/**
|
||||
* @return int Timeout in milliseconds for the HTTP transfer (not counting TCP connect and TLS handshake)
|
||||
*/
|
||||
public function getTransferTimeout() : int
|
||||
{
|
||||
return $this->transferTimeout;
|
||||
}
|
||||
public function setTransferTimeout(int $transferTimeout) : void
|
||||
{
|
||||
$this->transferTimeout = $transferTimeout;
|
||||
}
|
||||
/**
|
||||
* @return int Timeout in milliseconds since the last data was received before the request fails due to inactivity.
|
||||
*/
|
||||
public function getInactivityTimeout() : int
|
||||
{
|
||||
return $this->inactivityTimeout;
|
||||
}
|
||||
public function setInactivityTimeout(int $inactivityTimeout) : void
|
||||
{
|
||||
$this->inactivityTimeout = $inactivityTimeout;
|
||||
}
|
||||
public function getHeaderSizeLimit() : int
|
||||
{
|
||||
return $this->headerSizeLimit;
|
||||
}
|
||||
public function setHeaderSizeLimit(int $headerSizeLimit) : void
|
||||
{
|
||||
$this->headerSizeLimit = $headerSizeLimit;
|
||||
}
|
||||
public function getBodySizeLimit() : int
|
||||
{
|
||||
return $this->bodySizeLimit;
|
||||
}
|
||||
public function setBodySizeLimit(int $bodySizeLimit) : void
|
||||
{
|
||||
$this->bodySizeLimit = $bodySizeLimit;
|
||||
}
|
||||
/**
|
||||
* Note: This method returns a deep clone of the request's attributes, so you can't modify the request attributes
|
||||
* by modifying the returned value in any way.
|
||||
*
|
||||
* @return mixed[] An array of all request attributes in the request's local storage, indexed by name.
|
||||
*/
|
||||
public function getAttributes() : array
|
||||
{
|
||||
return self::clone($this->attributes);
|
||||
}
|
||||
/**
|
||||
* Check whether a variable with the given name exists in the request's local storage.
|
||||
*
|
||||
* Each request has its own local storage to which applications and interceptors may read and write data.
|
||||
* Other interceptors which are aware of this data can then access it without the server being tightly coupled to
|
||||
* specific implementations.
|
||||
*
|
||||
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAttribute(string $name) : bool
|
||||
{
|
||||
return \array_key_exists($name, $this->attributes);
|
||||
}
|
||||
/**
|
||||
* Retrieve a variable from the request's local storage.
|
||||
*
|
||||
* Each request has its own local storage to which applications and interceptors may read and write data.
|
||||
* Other interceptors which are aware of this data can then access it without the server being tightly coupled to
|
||||
* specific implementations.
|
||||
*
|
||||
* Note: This method returns a deep clone of the request's attribute, so you can't modify the request attribute
|
||||
* by modifying the returned value in any way.
|
||||
*
|
||||
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @throws MissingAttributeError If an attribute with the given name does not exist.
|
||||
*/
|
||||
public function getAttribute(string $name)
|
||||
{
|
||||
if (!$this->hasAttribute($name)) {
|
||||
throw new MissingAttributeError("The requested attribute '{$name}' does not exist");
|
||||
}
|
||||
return self::clone($this->attributes[$name]);
|
||||
}
|
||||
/**
|
||||
* Assign a variable to the request's local storage.
|
||||
*
|
||||
* Each request has its own local storage to which applications and interceptors may read and write data.
|
||||
* Other interceptors which are aware of this data can then access it without the server being tightly coupled to
|
||||
* specific implementations.
|
||||
*
|
||||
* Note: This method performs a deep clone of the value via serialization, so you can't modify the given value
|
||||
* after setting it.
|
||||
*
|
||||
* **Example**
|
||||
*
|
||||
* ```php
|
||||
* $request->setAttribute(Timing::class, $stopWatch);
|
||||
* ```
|
||||
*
|
||||
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
|
||||
* @param mixed $value Value of the attribute, might be any serializable value.
|
||||
*/
|
||||
public function setAttribute(string $name, $value) : void
|
||||
{
|
||||
$this->attributes[$name] = self::clone($value);
|
||||
}
|
||||
/**
|
||||
* Remove an attribute from the request's local storage.
|
||||
*
|
||||
* @param string $name Name of the attribute, should be namespaced with a vendor and package namespace like classes.
|
||||
*
|
||||
* @throws MissingAttributeError If an attribute with the given name does not exist.
|
||||
*/
|
||||
public function removeAttribute(string $name) : void
|
||||
{
|
||||
if (!$this->hasAttribute($name)) {
|
||||
throw new MissingAttributeError("The requested attribute '{$name}' does not exist");
|
||||
}
|
||||
unset($this->attributes[$name]);
|
||||
}
|
||||
/**
|
||||
* Remove all attributes from the request's local storage.
|
||||
*/
|
||||
public function removeAttributes() : void
|
||||
{
|
||||
$this->attributes = [];
|
||||
}
|
||||
public function isIdempotent() : bool
|
||||
{
|
||||
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
|
||||
return \in_array($this->method, ['GET', 'HEAD', 'PUT', 'DELETE'], \true);
|
||||
}
|
||||
private function createUriFromString(string $uri) : UriInterface
|
||||
{
|
||||
return Uri\Http::createFromString($uri);
|
||||
}
|
||||
}
|
34
dependencies/amphp/http-client/src/RequestBody.php
vendored
Normal file
34
dependencies/amphp/http-client/src/RequestBody.php
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
/**
|
||||
* An interface for generating HTTP message bodies + headers.
|
||||
*/
|
||||
interface RequestBody
|
||||
{
|
||||
/**
|
||||
* Retrieve a key-value array of headers to add to the outbound request.
|
||||
*
|
||||
* The resolved promise value must be a key-value array mapping header fields to values.
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function getHeaders() : Promise;
|
||||
/**
|
||||
* Create the HTTP message body to be sent.
|
||||
*
|
||||
* Further calls MUST return a new stream to make it possible to resend bodies on redirects.
|
||||
*
|
||||
* @return InputStream
|
||||
*/
|
||||
public function createBodyStream() : InputStream;
|
||||
/**
|
||||
* Retrieve the HTTP message body length. If not available, return -1.
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function getBodyLength() : Promise;
|
||||
}
|
227
dependencies/amphp/http-client/src/Response.php
vendored
Normal file
227
dependencies/amphp/http-client/src/Response.php
vendored
Normal file
@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InMemoryStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\InputStream;
|
||||
use WP_Ultimo\Dependencies\Amp\ByteStream\Payload;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidCloning;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Message;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Status;
|
||||
use WP_Ultimo\Dependencies\Amp\Promise;
|
||||
use WP_Ultimo\Dependencies\Amp\Success;
|
||||
/**
|
||||
* An HTTP response.
|
||||
*/
|
||||
final class Response extends Message
|
||||
{
|
||||
use ForbidSerialization;
|
||||
use ForbidCloning;
|
||||
/** @var string */
|
||||
private $protocolVersion;
|
||||
/** @var int */
|
||||
private $status;
|
||||
/** @var string */
|
||||
private $reason;
|
||||
/** @var Request */
|
||||
private $request;
|
||||
/** @var Payload */
|
||||
private $body;
|
||||
/** @var Promise<Trailers> */
|
||||
private $trailers;
|
||||
/** @var Response|null */
|
||||
private $previousResponse;
|
||||
public function __construct(string $protocolVersion, int $status, ?string $reason, array $headers, InputStream $body, Request $request, ?Promise $trailerPromise = null, ?Response $previousResponse = null)
|
||||
{
|
||||
$this->setProtocolVersion($protocolVersion);
|
||||
$this->setStatus($status, $reason);
|
||||
$this->setHeaders($headers);
|
||||
$this->setBody($body);
|
||||
$this->request = $request;
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
$this->trailers = $trailerPromise ?? new Success(new Trailers([]));
|
||||
$this->previousResponse = $previousResponse;
|
||||
}
|
||||
/**
|
||||
* Retrieve the requests's HTTP protocol version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getProtocolVersion() : string
|
||||
{
|
||||
return $this->protocolVersion;
|
||||
}
|
||||
public function setProtocolVersion(string $protocolVersion) : void
|
||||
{
|
||||
if (!\in_array($protocolVersion, ["1.0", "1.1", "2"], \true)) {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
throw new \Error("Invalid HTTP protocol version: " . $protocolVersion);
|
||||
}
|
||||
$this->protocolVersion = $protocolVersion;
|
||||
}
|
||||
/**
|
||||
* Retrieve the response's three-digit HTTP status code.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getStatus() : int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
public function setStatus(int $status, ?string $reason = null) : void
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->reason = $reason ?? Status::getReason($status);
|
||||
}
|
||||
/**
|
||||
* Retrieve the response's (possibly empty) reason phrase.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReason() : string
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
/**
|
||||
* Retrieve the Request instance that resulted in this Response instance.
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest() : Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
public function setRequest(Request $request) : void
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
/**
|
||||
* Retrieve the original Request instance associated with this Response instance.
|
||||
*
|
||||
* A given Response may be the result of one or more redirects. This method is a shortcut to
|
||||
* access information from the original Request that led to this response.
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
public function getOriginalRequest() : Request
|
||||
{
|
||||
if (empty($this->previousResponse)) {
|
||||
return $this->request;
|
||||
}
|
||||
return $this->previousResponse->getOriginalRequest();
|
||||
}
|
||||
/**
|
||||
* Retrieve the original Response instance associated with this Response instance.
|
||||
*
|
||||
* A given Response may be the result of one or more redirects. This method is a shortcut to
|
||||
* access information from the original Response that led to this response.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getOriginalResponse() : Response
|
||||
{
|
||||
if (empty($this->previousResponse)) {
|
||||
return $this;
|
||||
}
|
||||
return $this->previousResponse->getOriginalResponse();
|
||||
}
|
||||
/**
|
||||
* If this Response is the result of a redirect traverse up the redirect history.
|
||||
*
|
||||
* @return Response|null
|
||||
*/
|
||||
public function getPreviousResponse() : ?Response
|
||||
{
|
||||
return $this->previousResponse;
|
||||
}
|
||||
public function setPreviousResponse(?Response $previousResponse) : void
|
||||
{
|
||||
$this->previousResponse = $previousResponse;
|
||||
}
|
||||
/**
|
||||
* Assign a value for the specified header field by replacing any existing values for that field.
|
||||
*
|
||||
* @param string $name Header name.
|
||||
* @param string|string[] $value Header value.
|
||||
*/
|
||||
public function setHeader(string $name, $value) : void
|
||||
{
|
||||
if (($name[0] ?? ":") === ":") {
|
||||
throw new \Error("Header name cannot be empty or start with a colon (:)");
|
||||
}
|
||||
parent::setHeader($name, $value);
|
||||
}
|
||||
/**
|
||||
* Assign a value for the specified header field by adding an additional header line.
|
||||
*
|
||||
* @param string $name Header name.
|
||||
* @param string|string[] $value Header value.
|
||||
*/
|
||||
public function addHeader(string $name, $value) : void
|
||||
{
|
||||
if (($name[0] ?? ":") === ":") {
|
||||
throw new \Error("Header name cannot be empty or start with a colon (:)");
|
||||
}
|
||||
parent::addHeader($name, $value);
|
||||
}
|
||||
public function setHeaders(array $headers) : void
|
||||
{
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
parent::setHeaders($headers);
|
||||
}
|
||||
/**
|
||||
* Remove the specified header field from the message.
|
||||
*
|
||||
* @param string $name Header name.
|
||||
*/
|
||||
public function removeHeader(string $name) : void
|
||||
{
|
||||
parent::removeHeader($name);
|
||||
}
|
||||
/**
|
||||
* Retrieve the response body.
|
||||
*
|
||||
* Note: If you stream a Message, you can't consume the payload twice.
|
||||
*
|
||||
* @return Payload
|
||||
*/
|
||||
public function getBody() : Payload
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
/**
|
||||
* @param Payload|InputStream|string|int|float|bool $body
|
||||
*/
|
||||
public function setBody($body) : void
|
||||
{
|
||||
if ($body instanceof Payload) {
|
||||
$this->body = $body;
|
||||
} elseif ($body === null) {
|
||||
$this->body = new Payload(new InMemoryStream());
|
||||
} elseif (\is_string($body)) {
|
||||
$this->body = new Payload(new InMemoryStream($body));
|
||||
} elseif (\is_scalar($body)) {
|
||||
$this->body = new Payload(new InMemoryStream(\var_export($body, \true)));
|
||||
} elseif ($body instanceof InputStream) {
|
||||
$this->body = new Payload($body);
|
||||
} else {
|
||||
/** @noinspection PhpUndefinedClassInspection */
|
||||
throw new \TypeError("Invalid body type: " . \gettype($body));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @return Promise<Trailers>
|
||||
*/
|
||||
public function getTrailers() : Promise
|
||||
{
|
||||
return $this->trailers;
|
||||
}
|
||||
/**
|
||||
* @param Promise<Trailers> $promise
|
||||
*/
|
||||
public function setTrailers(Promise $promise) : void
|
||||
{
|
||||
$this->trailers = $promise;
|
||||
}
|
||||
}
|
7
dependencies/amphp/http-client/src/SocketException.php
vendored
Normal file
7
dependencies/amphp/http-client/src/SocketException.php
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
final class SocketException extends HttpException
|
||||
{
|
||||
}
|
7
dependencies/amphp/http-client/src/TimeoutException.php
vendored
Normal file
7
dependencies/amphp/http-client/src/TimeoutException.php
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
final class TimeoutException extends HttpException
|
||||
{
|
||||
}
|
27
dependencies/amphp/http-client/src/Trailers.php
vendored
Normal file
27
dependencies/amphp/http-client/src/Trailers.php
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace WP_Ultimo\Dependencies\Amp\Http\Client;
|
||||
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Client\Internal\ForbidSerialization;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\InvalidHeaderException;
|
||||
use WP_Ultimo\Dependencies\Amp\Http\Message;
|
||||
final class Trailers extends Message
|
||||
{
|
||||
use ForbidSerialization;
|
||||
/** @see https://tools.ietf.org/html/rfc7230#section-4.1.2 */
|
||||
public const DISALLOWED_TRAILERS = ["authorization" => \true, "content-encoding" => \true, "content-length" => \true, "content-range" => \true, "content-type" => \true, "cookie" => \true, "expect" => \true, "host" => \true, "pragma" => \true, "proxy-authenticate" => \true, "proxy-authorization" => \true, "range" => \true, "te" => \true, "trailer" => \true, "transfer-encoding" => \true, "www-authenticate" => \true];
|
||||
/**
|
||||
* @param string[]|string[][] $headers
|
||||
*
|
||||
* @throws InvalidHeaderException Thrown if a disallowed field is in the header values.
|
||||
*/
|
||||
public function __construct(array $headers)
|
||||
{
|
||||
if (!empty($headers)) {
|
||||
$this->setHeaders($headers);
|
||||
}
|
||||
if (\array_intersect_key($this->getHeaders(), self::DISALLOWED_TRAILERS)) {
|
||||
throw new InvalidHeaderException('Disallowed field in trailers');
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user