547 lines
25 KiB
PHP
547 lines
25 KiB
PHP
<?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();
|
|
}
|
|
});
|
|
}
|
|
}
|