From c996d2b0627f82192a2b993072e0bea0bc2c518a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 28 Jul 2023 13:00:02 +0200 Subject: [PATCH 01/14] Adapt for latest http-client --- .php_cs.dist => .php-cs-fixer.dist.php | 8 ++---- README.md | 38 ++++++++++++-------------- a.php | 29 ++++++++++++++++++++ composer.json | 6 ++-- examples/amp-with-guzzle.php | 2 +- examples/psr-with-amp.php | 2 +- phpunit.xml.dist | 28 ++++++++----------- psalm.xml.dist | 2 +- src/Internal/PsrInputStream.php | 8 ++++-- src/Internal/PsrMessageStream.php | 4 +-- src/Internal/PsrStreamBody.php | 2 +- src/PsrAdapter.php | 11 ++------ src/PsrHttpClient.php | 8 +----- test/Internal/PsrInputStreamTest.php | 7 +---- test/Internal/PsrMessageStreamTest.php | 11 +------- test/Internal/PsrStreamBodyTest.php | 4 +-- test/PsrAdapterTest.php | 28 ++++++++----------- 17 files changed, 95 insertions(+), 103 deletions(-) rename .php_cs.dist => .php-cs-fixer.dist.php (70%) create mode 100644 a.php diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 70% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index dbdd26d..133572d 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,14 +1,12 @@ getFinder() - ->in(__DIR__ . '/examples') - ->in(__DIR__ . '/src') - ->in(__DIR__ . '/test'); + ->in(__DIR__ . "/examples") + ->in(__DIR__ . "/src") + ->in(__DIR__ . "/test"); $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; - $config->setCacheFile($cacheDir . '/.php_cs.cache'); return $config; diff --git a/README.md b/README.md index 872b5e0..abff4e7 100644 --- a/README.md +++ b/README.md @@ -21,40 +21,38 @@ Create `Amp\Http\Client\Psr7\PsrAdapter` instance to convert client requests and ```php createRequest('GET', 'https://google.com/'); - $ampRequest = yield $psrAdapter->fromPsrRequest($psrRequest); +$psrAdapter = new PsrAdapter($psrRequestFactory, $psrResponseFactory); - // Convert Amp request to PSR-7 request - $psrRequest = yield $psrAdapter->toPsrRequest($psrRequestFactory, $ampRequest); +// Convert PSR-7 request to Amp request +$psrRequest = $psrRequestFactory->createRequest('GET', 'https://google.com/'); +$ampRequest = $psrAdapter->fromPsrRequest($psrRequest); - // Convert PSR-7 response to Amp response - $psrResponse = $psrResponseFactory->createResponse(); - $ampResponse = yield $psrAdapter->fromPsrResponse($psrResponse, $ampRequest); +// Convert Amp request to PSR-7 request +$psrRequest = $psrAdapter->toPsrRequest($ampRequest); - // Convert Amp response to PSR-7 response - $psrResponse = yield $psrAdapter->toPsrResponse($psrResponseFactory, $ampResponse); -}); +// Convert PSR-7 response to Amp response +$psrResponse = $psrResponseFactory->createResponse(); +$ampResponse = $psrAdapter->fromPsrResponse($psrResponse, $ampRequest); +// Convert Amp response to PSR-7 response +$psrResponse = $psrAdapter->toPsrResponse($ampResponse); ``` There are few incompatibilities between Amp and PSR-7 implementations that may require special handling: - PSR-7 requests contain only one protocol version, but Amp requests can contain several versions. In this case the adapter checks if the protocol version list contains a version that is the current PSR-7 implementation default, otherwise it throws an exception. You may also set the protocol version explicitly using the optional argument of the `toPsrRequest()` method. -- Amp responses contain a reference to the `Request` instance, but PSR-7 responses don't; so you need to provide a request instance explicitly. +- Amp responses contain a reference to the `Request` instance, but PSR-7 responses don't; so you need to provide a request instance explicitly. ## Examples diff --git a/a.php b/a.php new file mode 100644 index 0000000..c7bcbca --- /dev/null +++ b/a.php @@ -0,0 +1,29 @@ + +createRequest('GET', 'https://google.com/'); +$ampRequest = $psrAdapter->fromPsrRequest($psrRequest); + +// Convert Amp request to PSR-7 request +$psrRequest = $psrAdapter->toPsrRequest($ampRequest); + +// Convert PSR-7 response to Amp response +$psrResponse = $psrResponseFactory->createResponse(); +$ampResponse = $psrAdapter->fromPsrResponse($psrResponse, $ampRequest); + +// Convert Amp response to PSR-7 response +$psrResponse = $psrAdapter->toPsrResponse($ampResponse); diff --git a/composer.json b/composer.json index ffb623c..ce47e71 100644 --- a/composer.json +++ b/composer.json @@ -35,11 +35,11 @@ }, "require-dev": { "amphp/phpunit-util": "^1.4", - "amphp/php-cs-fixer-config": "dev-master", + "amphp/php-cs-fixer-config": "^2", "phpunit/phpunit": "^9", - "friendsofphp/php-cs-fixer": "^2.3", "laminas/laminas-diactoros": "^2.3", - "guzzlehttp/guzzle": "^7" + "guzzlehttp/guzzle": "^7", + "psalm/phar": "^5" }, "autoload": { "psr-4": { diff --git a/examples/amp-with-guzzle.php b/examples/amp-with-guzzle.php index e35dc98..dad9462 100644 --- a/examples/amp-with-guzzle.php +++ b/examples/amp-with-guzzle.php @@ -1,4 +1,4 @@ - - - - - test - - - - - src - - + + + + src + + + + + test + + - diff --git a/psalm.xml.dist b/psalm.xml.dist index ee41085..15b8153 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -1,6 +1,6 @@ */ -final class PsrInputStream implements ReadableStream +final class PsrInputStream implements ReadableStream, \IteratorAggregate { + use ReadableStreamIteratorAggregate; public const DEFAULT_CHUNK_SIZE = 8192; private StreamInterface $stream; diff --git a/src/Internal/PsrMessageStream.php b/src/Internal/PsrMessageStream.php index d11cdb7..270ed8e 100644 --- a/src/Internal/PsrMessageStream.php +++ b/src/Internal/PsrMessageStream.php @@ -1,4 +1,4 @@ -timeout = $timeout; } - public function __toString() + public function __toString(): string { try { return $this->getContents(); diff --git a/src/Internal/PsrStreamBody.php b/src/Internal/PsrStreamBody.php index 68edf77..43da7d7 100644 --- a/src/Internal/PsrStreamBody.php +++ b/src/Internal/PsrStreamBody.php @@ -1,4 +1,4 @@ -responseFactory->createResponse($response->getStatus(), $response->getReason()) ->withProtocolVersion($response->getProtocolVersion()); - foreach ($response->getRawHeaders() as [$headerName, $headerValue]) { + foreach ($response->getHeaderPairs() as [$headerName, $headerValue]) { $psrResponse = $psrResponse->withAddedHeader($headerName, $headerValue); } @@ -92,7 +87,7 @@ private function toPsrRequestWithoutBody( ): PsrRequest { $target = $this->requestFactory->createRequest($source->getMethod(), $source->getUri()); - foreach ($source->getRawHeaders() as [$headerName, $headerValue]) { + foreach ($source->getHeaderPairs() as [$headerName, $headerValue]) { $target = $target->withAddedHeader($headerName, $headerValue); } diff --git a/src/PsrHttpClient.php b/src/PsrHttpClient.php index f884487..67bcc2f 100644 --- a/src/PsrHttpClient.php +++ b/src/PsrHttpClient.php @@ -1,4 +1,4 @@ -psrAdapter = $psrAdapter; } - /** - * @param PsrRequest $request - * @param Cancellation|null $cancellation - * - * @return PsrResponse - */ public function sendRequest(PsrRequest $request, ?Cancellation $cancellation = null): PsrResponse { $internalRequest = $this->psrAdapter->fromPsrRequest($request); diff --git a/test/Internal/PsrInputStreamTest.php b/test/Internal/PsrInputStreamTest.php index f24b1ad..d04a1f7 100644 --- a/test/Internal/PsrInputStreamTest.php +++ b/test/Internal/PsrInputStreamTest.php @@ -1,4 +1,4 @@ -toPsrResponse($source); - self::assertSame(Status::NOT_FOUND, $target->getStatusCode()); + self::assertSame(HttpStatus::NOT_FOUND, $target->getStatusCode()); } public function testToPsrResponseReturnsResponseWithEqualReason(): void @@ -206,7 +202,7 @@ public function testToPsrResponseReturnsResponseWithEqualReason(): void $source = new Response( '1.1', - Status::OK, + HttpStatus::OK, 'a', [], new ReadableBuffer(''), @@ -224,7 +220,7 @@ public function testToPsrResponseReturnsResponseWithEqualHeaders(): void $source = new Response( '1.1', - Status::OK, + HttpStatus::OK, null, ['a' => 'b', 'c' => ['d', 'e']], new ReadableBuffer(''), @@ -242,7 +238,7 @@ public function testToPsrResponseReturnsResponseWithEqualBody(): void $source = new Response( '1.1', - Status::OK, + HttpStatus::OK, null, [], new ReadableBuffer('body_content'), @@ -286,7 +282,7 @@ public function testFromPsrResponseWithPreviousResponseReturnsResponseWithSamePr $previousResponse = new Response( '1.1', - Status::OK, + HttpStatus::OK, null, [], new ReadableBuffer(''), @@ -315,11 +311,11 @@ public function testFromPsrResponseReturnsResultWithEqualStatus(): void { $adapter = new PsrAdapter(new RequestFactory, new ResponseFactory); - $source = (new PsrResponse())->withStatus(Status::NOT_FOUND); + $source = (new PsrResponse())->withStatus(HttpStatus::NOT_FOUND); $target = $adapter->fromPsrResponse($source, new Request('')); - self::assertSame(Status::NOT_FOUND, $target->getStatus()); + self::assertSame(HttpStatus::NOT_FOUND, $target->getStatus()); } public function testFromPsrResponseReturnsResultWithEqualHeaders(): void @@ -328,7 +324,7 @@ public function testFromPsrResponseReturnsResultWithEqualHeaders(): void $source = new PsrResponse( 'php://memory', - Status::OK, + HttpStatus::OK, ['a' => 'b', 'c' => ['d', 'e']] ); From a65379b0ba6d4317fdd707931c704520300cc559 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 1 Aug 2023 09:35:18 +0200 Subject: [PATCH 02/14] Adapt and add guzzle AmpHandler --- composer.json | 4 +- examples/amp-with-guzzle.php | 18 ++++---- src/AmpHandler.php | 61 +++++++++++++++++++++++++ src/Internal/PsrStreamBody.php | 11 ++--- src/PsrAdapter.php | 2 +- test/GuzzleAdapterTest.php | 69 +++++++++++++++++++++++++++++ test/Internal/PsrStreamBodyTest.php | 10 ++--- test/PsrAdapterTest.php | 5 ++- 8 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 src/AmpHandler.php create mode 100644 test/GuzzleAdapterTest.php diff --git a/composer.json b/composer.json index ce47e71..9da95ba 100644 --- a/composer.json +++ b/composer.json @@ -31,10 +31,10 @@ "psr/http-message": "^1", "psr/http-factory": "^1", "psr/http-client": "^1", - "revolt/event-loop": "^0.2" + "revolt/event-loop": "^1" }, "require-dev": { - "amphp/phpunit-util": "^1.4", + "amphp/phpunit-util": "^3", "amphp/php-cs-fixer-config": "^2", "phpunit/phpunit": "^9", "laminas/laminas-diactoros": "^2.3", diff --git a/examples/amp-with-guzzle.php b/examples/amp-with-guzzle.php index dad9462..dd5648c 100644 --- a/examples/amp-with-guzzle.php +++ b/examples/amp-with-guzzle.php @@ -1,18 +1,18 @@ HandlerStack::create(new AmpHandler)]); -$request = new Request('https://api.github.com/'); +$future = async($client->get(...), 'https://api.github.com/', ['delay' => 1000]); -$psrResponse = (new Client)->send($psrAdapter->toPsrRequest($request)); -$response = $psrAdapter->fromPsrResponse($psrResponse, $request); +getStdout()->write("First output: ".$client->get('https://api.github.com/')->getBody().PHP_EOL); -print $response->getBody()->buffer(); +getStdout()->write("Deferred output: ".$future->await()->getBody().PHP_EOL); diff --git a/src/AmpHandler.php b/src/AmpHandler.php new file mode 100644 index 0000000..a11420c --- /dev/null +++ b/src/AmpHandler.php @@ -0,0 +1,61 @@ +client = $client ?? HttpClientBuilder::buildDefault(); + $this->psrAdapter = new PsrAdapter(new RequestFactory, new ResponseFactory); + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $deferred = new DeferredCancellation; + $cancellation = $deferred->getCancellation(); + $future = async(function () use ($request, $options, $cancellation) { + if (isset($options['delay'])) { + delay($options['delay'] / 1000.0, cancellation: $cancellation); + } + return $this->psrAdapter->toPsrResponse( + $this->client->request( + $this->psrAdapter->fromPsrRequest($request), + $cancellation + ) + ); + }); + $future->ignore(); + $promise = new Promise(function () use ($future, $cancellation, &$promise) { + try { + $promise->resolve($future->await()); + } catch (CancelledException $e) { + if (!$cancellation->isRequested()) { + $promise->reject($e); + } + } catch (Throwable $e) { + $promise->reject($e); + } + }, $deferred->cancel(...)); + return $promise; + } +} diff --git a/src/Internal/PsrStreamBody.php b/src/Internal/PsrStreamBody.php index 43da7d7..cbd0513 100644 --- a/src/Internal/PsrStreamBody.php +++ b/src/Internal/PsrStreamBody.php @@ -3,13 +3,14 @@ namespace Amp\Http\Client\Psr7\Internal; use Amp\ByteStream\ReadableStream; +use Amp\Http\Client\HttpContent; use Amp\Http\Client\RequestBody; use Psr\Http\Message\StreamInterface; /** * @internal */ -final class PsrStreamBody implements RequestBody +final class PsrStreamBody implements HttpContent { private StreamInterface $stream; @@ -18,18 +19,18 @@ public function __construct(StreamInterface $stream) $this->stream = $stream; } - public function createBodyStream(): ReadableStream + public function getContent(): ReadableStream { return new PsrInputStream($this->stream); } - public function getBodyLength(): ?int + public function getContentLength(): ?int { return $this->stream->getSize() ?? -1; } - public function getHeaders(): array + public function getContentType(): ?string { - return []; + return null; } } diff --git a/src/PsrAdapter.php b/src/PsrAdapter.php index 5c48074..d4646f3 100644 --- a/src/PsrAdapter.php +++ b/src/PsrAdapter.php @@ -53,7 +53,7 @@ public function toPsrRequest(Request $source, ?string $protocolVersion = null): { $target = $this->toPsrRequestWithoutBody($source, $protocolVersion); - $this->copyToPsrStream($source->getBody()->createBodyStream(), $target->getBody()); + $this->copyToPsrStream($source->getBody()->getContent(), $target->getBody()); return $target; } diff --git a/test/GuzzleAdapterTest.php b/test/GuzzleAdapterTest.php new file mode 100644 index 0000000..911e9ec --- /dev/null +++ b/test/GuzzleAdapterTest.php @@ -0,0 +1,69 @@ + HandlerStack::create(new AmpHandler)]); + $this->assertNotEmpty((string) $client->get('https://example.com/')->getBody()); + } + public function testRequestDelay(): void + { + $client = new Client(['handler' => HandlerStack::create(new AmpHandler)]); + $future = async($client->get(...), 'https://example.com/', ['delay' => 1000]); + $this->assertFalse($future->isComplete()); + delay(1); + $t = microtime(true); + $this->assertNotEmpty((string) $future->await()->getBody()); + $this->assertTrue(microtime(true)-$t < 1); + } + public function testRequestDelayGuzzleAsync(): void + { + $client = new Client(['handler' => HandlerStack::create(new AmpHandler)]); + $promise = $client->getAsync('https://example.com/', ['delay' => 1000]); + $this->assertEquals($promise->getState(), PromiseInterface::PENDING); + delay(1); + $t = microtime(true); + $this->assertNotEmpty((string) $promise->wait()->getBody()); + $this->assertTrue(microtime(true)-$t < 1); + } + public function testRequestCancel(): void + { + $client = new Client(['handler' => HandlerStack::create(new AmpHandler)]); + $promise = $client->getAsync('https://example.com/', ['delay' => 2000]); + $promise->cancel(); + $this->assertEquals($promise->getState(), PromiseInterface::REJECTED); + } + public function testRequest404(): void + { + $this->expectExceptionMessageMatches('/404 Not Found/'); + $client = new Client(['handler' => HandlerStack::create(new AmpHandler)]); + $client->get('https://example.com/test'); + } +} diff --git a/test/Internal/PsrStreamBodyTest.php b/test/Internal/PsrStreamBodyTest.php index d1c3947..f1f0b3c 100644 --- a/test/Internal/PsrStreamBodyTest.php +++ b/test/Internal/PsrStreamBodyTest.php @@ -13,8 +13,6 @@ class PsrStreamBodyTest extends TestCase { /** - * - * @return \Generator * @dataProvider providerBodyLength */ public function testGetBodyLengthReturnsValueFromStream(?int $size, int $expectedSize): void @@ -24,7 +22,7 @@ public function testGetBodyLengthReturnsValueFromStream(?int $size, int $expecte $body = new PsrStreamBody($stream); - self::assertSame($expectedSize, $body->getBodyLength()); + self::assertSame($expectedSize, $body->getContentLength()); } public function providerBodyLength(): array @@ -36,12 +34,12 @@ public function providerBodyLength(): array ]; } - public function testGetHeadersReturnsEmptyList(): void + public function testContentTypeIsNull(): void { $stream = $this->createMock(StreamInterface::class); $body = new PsrStreamBody($stream); - self::assertSame([], $body->getHeaders()); + self::assertSame(null, $body->getContentType()); } public function testCreateBodyStreamResultReadsFromOriginalStream(): void @@ -49,6 +47,6 @@ public function testCreateBodyStreamResultReadsFromOriginalStream(): void $stream = (new StreamFactory())->createStream('body_content'); $body = new PsrStreamBody($stream); - self::assertSame('body_content', buffer($body->createBodyStream())); + self::assertSame('body_content', buffer($body->getContent())); } } diff --git a/test/PsrAdapterTest.php b/test/PsrAdapterTest.php index b20030f..3273924 100644 --- a/test/PsrAdapterTest.php +++ b/test/PsrAdapterTest.php @@ -3,6 +3,7 @@ namespace Amp\Http\Client\Psr7; use Amp\ByteStream\ReadableBuffer; +use Amp\Http\Client\HttpContent; use Amp\Http\Client\HttpException; use Amp\Http\Client\Request; use Amp\Http\Client\RequestBody; @@ -347,9 +348,9 @@ public function testFromPsrResponseReturnsResultWithEqualBody(): void self::assertSame('body_content', $target->getBody()->buffer()); } - private function readBody(RequestBody $body): string + private function readBody(HttpContent $body): string { - $stream = $body->createBodyStream(); + $stream = $body->getContent(); return buffer($stream); } From 9de37666b9974994b09eab5ee06e64b9879e0732 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 1 Aug 2023 09:38:46 +0200 Subject: [PATCH 03/14] Cleanup --- a.php | 29 ----------------------------- 1 file changed, 29 deletions(-) delete mode 100644 a.php diff --git a/a.php b/a.php deleted file mode 100644 index c7bcbca..0000000 --- a/a.php +++ /dev/null @@ -1,29 +0,0 @@ - -createRequest('GET', 'https://google.com/'); -$ampRequest = $psrAdapter->fromPsrRequest($psrRequest); - -// Convert Amp request to PSR-7 request -$psrRequest = $psrAdapter->toPsrRequest($ampRequest); - -// Convert PSR-7 response to Amp response -$psrResponse = $psrResponseFactory->createResponse(); -$ampResponse = $psrAdapter->fromPsrResponse($psrResponse, $ampRequest); - -// Convert Amp response to PSR-7 response -$psrResponse = $psrAdapter->toPsrResponse($ampResponse); From 6ac3f254e77447505901512a8b94101a0250865a Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 1 Aug 2023 10:12:55 +0200 Subject: [PATCH 04/14] Add support for timeout options --- src/AmpHandler.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index a11420c..c7ecbf8 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -37,9 +37,16 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte if (isset($options['delay'])) { delay($options['delay'] / 1000.0, cancellation: $cancellation); } + $request = $this->psrAdapter->fromPsrRequest($request); + if (isset($options['timeout'])) { + $request->setTransferTimeout((float) $options['timeout']); + } + if (isset($options['connect_timeout'])) { + $request->setTcpConnectTimeout((float) $options['connect_timeout']); + } return $this->psrAdapter->toPsrResponse( $this->client->request( - $this->psrAdapter->fromPsrRequest($request), + $request, $cancellation ) ); From 7cf81c4c89c1b13d41bbd8785a4998ebd83c4d7d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 1 Aug 2023 10:33:19 +0200 Subject: [PATCH 05/14] Remove useless deps --- src/AmpHandler.php | 35 +++++++++++++++++++++-------- src/Internal/PsrStreamBody.php | 1 - test/GuzzleAdapterTest.php | 21 ++++------------- test/Internal/PsrStreamBodyTest.php | 2 +- test/PsrAdapterTest.php | 1 - 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index c7ecbf8..90dfec0 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -1,4 +1,4 @@ -client = $client ?? HttpClientBuilder::buildDefault(); - $this->psrAdapter = new PsrAdapter(new RequestFactory, new ResponseFactory); + self::$psrAdapter ??= new PsrAdapter(new class implements RequestFactoryInterface { + public function createRequest(string $method, $uri): RequestInterface + { + return new Request($method, $uri); + } + }, new class implements ResponseFactoryInterface { + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + return new Response($code, reason: $reasonPhrase); + } + }); } public function __invoke(RequestInterface $request, array $options): PromiseInterface @@ -37,14 +54,14 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte if (isset($options['delay'])) { delay($options['delay'] / 1000.0, cancellation: $cancellation); } - $request = $this->psrAdapter->fromPsrRequest($request); + $request = self::$psrAdapter->fromPsrRequest($request); if (isset($options['timeout'])) { $request->setTransferTimeout((float) $options['timeout']); } if (isset($options['connect_timeout'])) { $request->setTcpConnectTimeout((float) $options['connect_timeout']); } - return $this->psrAdapter->toPsrResponse( + return self::$psrAdapter->toPsrResponse( $this->client->request( $request, $cancellation diff --git a/src/Internal/PsrStreamBody.php b/src/Internal/PsrStreamBody.php index cbd0513..f325acd 100644 --- a/src/Internal/PsrStreamBody.php +++ b/src/Internal/PsrStreamBody.php @@ -4,7 +4,6 @@ use Amp\ByteStream\ReadableStream; use Amp\Http\Client\HttpContent; -use Amp\Http\Client\RequestBody; use Psr\Http\Message\StreamInterface; /** diff --git a/test/GuzzleAdapterTest.php b/test/GuzzleAdapterTest.php index 911e9ec..45bab97 100644 --- a/test/GuzzleAdapterTest.php +++ b/test/GuzzleAdapterTest.php @@ -2,25 +2,12 @@ namespace Amp\Http\Client\Psr7; -use Amp\ByteStream\ReadableBuffer; -use Amp\Http\Client\HttpContent; -use Amp\Http\Client\HttpException; -use Amp\Http\Client\Request; -use Amp\Http\Client\RequestBody; -use Amp\Http\Client\Response; -use Amp\Http\HttpStatus; use Amp\PHPUnit\AsyncTestCase; use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Promise\PromiseInterface; -use Laminas\Diactoros\Request as PsrRequest; -use Laminas\Diactoros\RequestFactory; -use Laminas\Diactoros\Response as PsrResponse; -use Laminas\Diactoros\ResponseFactory; -use PHPUnit\Framework\TestCase; use function Amp\async; -use function Amp\ByteStream\buffer; use function Amp\delay; /** @@ -39,9 +26,9 @@ public function testRequestDelay(): void $future = async($client->get(...), 'https://example.com/', ['delay' => 1000]); $this->assertFalse($future->isComplete()); delay(1); - $t = microtime(true); + $t = \microtime(true); $this->assertNotEmpty((string) $future->await()->getBody()); - $this->assertTrue(microtime(true)-$t < 1); + $this->assertTrue(\microtime(true)-$t < 1); } public function testRequestDelayGuzzleAsync(): void { @@ -49,9 +36,9 @@ public function testRequestDelayGuzzleAsync(): void $promise = $client->getAsync('https://example.com/', ['delay' => 1000]); $this->assertEquals($promise->getState(), PromiseInterface::PENDING); delay(1); - $t = microtime(true); + $t = \microtime(true); $this->assertNotEmpty((string) $promise->wait()->getBody()); - $this->assertTrue(microtime(true)-$t < 1); + $this->assertTrue(\microtime(true)-$t < 1); } public function testRequestCancel(): void { diff --git a/test/Internal/PsrStreamBodyTest.php b/test/Internal/PsrStreamBodyTest.php index f1f0b3c..31f32a8 100644 --- a/test/Internal/PsrStreamBodyTest.php +++ b/test/Internal/PsrStreamBodyTest.php @@ -39,7 +39,7 @@ public function testContentTypeIsNull(): void $stream = $this->createMock(StreamInterface::class); $body = new PsrStreamBody($stream); - self::assertSame(null, $body->getContentType()); + self::assertNull($body->getContentType()); } public function testCreateBodyStreamResultReadsFromOriginalStream(): void diff --git a/test/PsrAdapterTest.php b/test/PsrAdapterTest.php index 3273924..0aeab54 100644 --- a/test/PsrAdapterTest.php +++ b/test/PsrAdapterTest.php @@ -6,7 +6,6 @@ use Amp\Http\Client\HttpContent; use Amp\Http\Client\HttpException; use Amp\Http\Client\Request; -use Amp\Http\Client\RequestBody; use Amp\Http\Client\Response; use Amp\Http\HttpStatus; use Laminas\Diactoros\Request as PsrRequest; From d84f0240929dceb7d1f8c09b667217f86360dcf6 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Tue, 1 Aug 2023 11:07:30 +0200 Subject: [PATCH 06/14] Fixup psalm --- src/AmpHandler.php | 1 + src/PsrAdapter.php | 3 +++ 2 files changed, 4 insertions(+) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index 90dfec0..e36e3b9 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -54,6 +54,7 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte if (isset($options['delay'])) { delay($options['delay'] / 1000.0, cancellation: $cancellation); } + /** @psalm-suppress PossiblyNullReference Initialized in the constructor */ $request = self::$psrAdapter->fromPsrRequest($request); if (isset($options['timeout'])) { $request->setTransferTimeout((float) $options['timeout']); diff --git a/src/PsrAdapter.php b/src/PsrAdapter.php index d4646f3..0b55654 100644 --- a/src/PsrAdapter.php +++ b/src/PsrAdapter.php @@ -27,8 +27,10 @@ public function __construct(PsrRequestFactory $requestFactory, PsrResponseFactor public function fromPsrRequest(PsrRequest $source): Request { + /** @psalm-suppress ArgumentTypeCoercion Wrong typehints in PSR */ $target = new Request($source->getUri(), $source->getMethod()); $target->setHeaders($source->getHeaders()); + /** @psalm-suppress ArgumentTypeCoercion Wrong typehints in PSR */ $target->setProtocolVersions([$source->getProtocolVersion()]); $target->setBody(new PsrStreamBody($source->getBody())); @@ -37,6 +39,7 @@ public function fromPsrRequest(PsrRequest $source): Request public function fromPsrResponse(PsrResponse $source, Request $request, ?Response $previousResponse = null): Response { + /** @psalm-suppress ArgumentTypeCoercion Wrong typehints in PSR */ return new Response( $source->getProtocolVersion(), $source->getStatusCode(), From e68822c10cfa97a6b21c5bd7455c6037fe3664ce Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 2 Aug 2023 10:48:07 +0200 Subject: [PATCH 07/14] Implement proxy and remaining options except decode_content and socks proxies --- composer.json | 3 +- src/AmpHandler.php | 91 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 87 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 9da95ba..cb0655d 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,8 @@ "psr/http-message": "^1", "psr/http-factory": "^1", "psr/http-client": "^1", - "revolt/event-loop": "^1" + "revolt/event-loop": "^1", + "amphp/http-tunnel": "^2.0@beta" }, "require-dev": { "amphp/phpunit-util": "^3", diff --git a/src/AmpHandler.php b/src/AmpHandler.php index e36e3b9..7d96148 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -4,13 +4,23 @@ use Amp\CancelledException; use Amp\DeferredCancellation; +use Amp\Http\Client\Connection\DefaultConnectionFactory; +use Amp\Http\Client\Connection\UnlimitedConnectionPool; use Amp\Http\Client\HttpClient; use Amp\Http\Client\HttpClientBuilder; +use Amp\Http\Tunnel\Http1TunnelConnector; +use Amp\Http\Tunnel\Https1TunnelConnector; +use Amp\Socket\Certificate; +use Amp\Socket\ClientTlsContext; +use Amp\Socket\ConnectContext; use AssertionError; use GuzzleHttp\Promise\Promise; use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\RequestOptions; +use GuzzleHttp\Utils; use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseFactoryInterface; @@ -32,7 +42,11 @@ public function __construct(?HttpClient $client = null) if (!\interface_exists(PromiseInterface::class)) { throw new AssertionError("Please require guzzle to use the guzzle AmpHandler!"); } - $this->client = $client ?? HttpClientBuilder::buildDefault(); + $this->client = $client ?? ( + (new HttpClientBuilder) + ->followRedirects(0) + ->build() + ); self::$psrAdapter ??= new PsrAdapter(new class implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { @@ -56,14 +70,79 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte } /** @psalm-suppress PossiblyNullReference Initialized in the constructor */ $request = self::$psrAdapter->fromPsrRequest($request); - if (isset($options['timeout'])) { - $request->setTransferTimeout((float) $options['timeout']); + if (isset($options[RequestOptions::TIMEOUT])) { + $request->setTransferTimeout((float) $options[RequestOptions::TIMEOUT]); } - if (isset($options['connect_timeout'])) { - $request->setTcpConnectTimeout((float) $options['connect_timeout']); + if (isset($options[RequestOptions::CONNECT_TIMEOUT])) { + $request->setTcpConnectTimeout((float) $options[RequestOptions::CONNECT_TIMEOUT]); + } + if (isset($options[RequestOptions::PROXY])) { + } + + $client = $this->client; + if (isset($options[RequestOptions::CERT]) || + isset($options[RequestOptions::PROXY]) || ( + isset($options[RequestOptions::VERIFY]) + && $options[RequestOptions::VERIFY] !== true + )) { + $tlsContext = new ClientTlsContext(); + if (isset($options[RequestOptions::CERT])) { + if (\is_string($options[RequestOptions::CERT])) { + $tlsContext = $tlsContext->withCertificate(new Certificate( + $options[RequestOptions::CERT], + $options[RequestOptions::SSL_KEY] ?? null, + )); + } else { + $tlsContext = $tlsContext->withCertificate(new Certificate( + $options[RequestOptions::CERT][0], + $options[RequestOptions::SSL_KEY] ?? null, + $options[RequestOptions::CERT][1] + )); + } + } + if (isset($options[RequestOptions::VERIFY])) { + if ($options[RequestOptions::VERIFY] === false) { + $tlsContext = $tlsContext->withoutPeerVerification(); + } else { + $tlsContext = $tlsContext->withCaFile($options[RequestOptions::VERIFY]); + } + } + + $connector = null; + if (isset($options[RequestOptions::PROXY])) { + if (!\is_array($options['proxy'])) { + $connector = $options['proxy']; + } else { + $scheme = $request->getUri()->getScheme(); + if (isset($options['proxy'][$scheme])) { + $host = $request->getUri()->getHost(); + if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) { + $connector = $options['proxy'][$scheme]; + } + } + } + + if ($connector !== null) { + $connector = new Uri($connector); + $connector = match ($connector->getScheme()) { + 'http' => new Http1TunnelConnector($connector->getHost().':'.$connector->getPort()), + 'https' => new Https1TunnelConnector($connector->getHost().':'.$connector->getPort(), new ClientTlsContext()), + }; + } + } + + $connectContext = (new ConnectContext) + ->withTlsContext($tlsContext); + + $client = (new HttpClientBuilder) + ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(connector: $connector, connectContext: $connectContext))) + ->build(); + } + if (isset($options['amp']['protocols'])) { + $request->setProtocolVersions($options['amp']['protocols']); } return self::$psrAdapter->toPsrResponse( - $this->client->request( + $client->request( $request, $cancellation ) From 0325bea478d7b22a1b059fb211ffe20a040f3d5e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 3 Aug 2023 10:04:37 +0200 Subject: [PATCH 08/14] Add socks5 support --- composer.json | 4 +++- psalm-baseline.xml | 15 +++++++++++++++ src/AmpHandler.php | 16 +++++++++++----- test/GuzzleAdapterTest.php | 18 ++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 psalm-baseline.xml diff --git a/composer.json b/composer.json index cb0655d..d7326b4 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,9 @@ "phpunit/phpunit": "^9", "laminas/laminas-diactoros": "^2.3", "guzzlehttp/guzzle": "^7", - "psalm/phar": "^5" + "psalm/phar": "^5", + "leproxy/leproxy": "^0.2.2", + "revolt/event-loop-adapter-react": "^1" }, "autoload": { "psr-4": { diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..8950025 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,15 @@ + + + + + fromPsrRequest + + + + + getMethod()]]> + getProtocolVersion()]]> + getProtocolVersion()]]]> + + + diff --git a/src/AmpHandler.php b/src/AmpHandler.php index 7d96148..e87ce8c 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -10,6 +10,7 @@ use Amp\Http\Client\HttpClientBuilder; use Amp\Http\Tunnel\Http1TunnelConnector; use Amp\Http\Tunnel\Https1TunnelConnector; +use Amp\Http\Tunnel\Socks5TunnelConnector; use Amp\Socket\Certificate; use Amp\Socket\ClientTlsContext; use Amp\Socket\ConnectContext; @@ -85,8 +86,9 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte isset($options[RequestOptions::VERIFY]) && $options[RequestOptions::VERIFY] !== true )) { - $tlsContext = new ClientTlsContext(); + $tlsContext = null; if (isset($options[RequestOptions::CERT])) { + $tlsContext ??= new ClientTlsContext(); if (\is_string($options[RequestOptions::CERT])) { $tlsContext = $tlsContext->withCertificate(new Certificate( $options[RequestOptions::CERT], @@ -101,9 +103,10 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte } } if (isset($options[RequestOptions::VERIFY])) { + $tlsContext ??= new ClientTlsContext(); if ($options[RequestOptions::VERIFY] === false) { $tlsContext = $tlsContext->withoutPeerVerification(); - } else { + } elseif (\is_string($options[RequestOptions::VERIFY])) { $tlsContext = $tlsContext->withCaFile($options[RequestOptions::VERIFY]); } } @@ -126,13 +129,16 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte $connector = new Uri($connector); $connector = match ($connector->getScheme()) { 'http' => new Http1TunnelConnector($connector->getHost().':'.$connector->getPort()), - 'https' => new Https1TunnelConnector($connector->getHost().':'.$connector->getPort(), new ClientTlsContext()), + 'https' => new Https1TunnelConnector($connector->getHost().':'.$connector->getPort(), new ClientTlsContext($connector->getHost())), + 'socks5' => new Socks5TunnelConnector($connector->getHost().':'.$connector->getPort()) }; } } - $connectContext = (new ConnectContext) - ->withTlsContext($tlsContext); + $connectContext = new ConnectContext; + if ($tlsContext) { + $connectContext = $connectContext->withTlsContext($tlsContext); + } $client = (new HttpClientBuilder) ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(connector: $connector, connectContext: $connectContext))) diff --git a/test/GuzzleAdapterTest.php b/test/GuzzleAdapterTest.php index 45bab97..fd8fecb 100644 --- a/test/GuzzleAdapterTest.php +++ b/test/GuzzleAdapterTest.php @@ -6,6 +6,9 @@ use GuzzleHttp\Client; use GuzzleHttp\HandlerStack; use GuzzleHttp\Promise\PromiseInterface; +use GuzzleHttp\RequestOptions; +use LeProxy\LeProxy\LeProxyServer; +use React\EventLoop\Loop; use function Amp\async; use function Amp\delay; @@ -30,6 +33,21 @@ public function testRequestDelay(): void $this->assertNotEmpty((string) $future->await()->getBody()); $this->assertTrue(\microtime(true)-$t < 1); } + public function testRequestProxies(): void + { + $proxy = new LeProxyServer(Loop::get()); + $socket = $proxy->listen('127.0.0.1:0', false); + + $client = new Client(['handler' => HandlerStack::create(new AmpHandler)]); + foreach (['socks5://', 'http://'] as $scheme) { + $uri = \str_replace('tcp://', $scheme, $socket->getAddress()); + + $result = $client->get('https://example.com/', [RequestOptions::PROXY => [ + 'https' => $uri + ]]); + $this->assertStringContainsString('Example Domain', (string) $result->getBody()); + } + } public function testRequestDelayGuzzleAsync(): void { $client = new Client(['handler' => HandlerStack::create(new AmpHandler)]); From 19e68f21bbe037a0a36823e82a600217b7a1f3d2 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 3 Aug 2023 11:04:45 +0200 Subject: [PATCH 09/14] Fix? --- src/AmpHandler.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index e87ce8c..76e6617 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -73,6 +73,7 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte $request = self::$psrAdapter->fromPsrRequest($request); if (isset($options[RequestOptions::TIMEOUT])) { $request->setTransferTimeout((float) $options[RequestOptions::TIMEOUT]); + $request->setInactivityTimeout((float) $options[RequestOptions::TIMEOUT]); } if (isset($options[RequestOptions::CONNECT_TIMEOUT])) { $request->setTcpConnectTimeout((float) $options[RequestOptions::CONNECT_TIMEOUT]); From 1489b21ce6a16b3a4287618102cf36faf40c609f Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 3 Aug 2023 13:54:40 +0200 Subject: [PATCH 10/14] Implement DNS type restriction --- src/AmpHandler.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index 76e6617..22b720d 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -4,6 +4,7 @@ use Amp\CancelledException; use Amp\DeferredCancellation; +use Amp\Dns\DnsRecord; use Amp\Http\Client\Connection\DefaultConnectionFactory; use Amp\Http\Client\Connection\UnlimitedConnectionPool; use Amp\Http\Client\HttpClient; @@ -78,15 +79,15 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte if (isset($options[RequestOptions::CONNECT_TIMEOUT])) { $request->setTcpConnectTimeout((float) $options[RequestOptions::CONNECT_TIMEOUT]); } - if (isset($options[RequestOptions::PROXY])) { - } $client = $this->client; if (isset($options[RequestOptions::CERT]) || isset($options[RequestOptions::PROXY]) || ( isset($options[RequestOptions::VERIFY]) && $options[RequestOptions::VERIFY] !== true - )) { + ) || + isset($options[RequestOptions::FORCE_IP_RESOLVE]) + ) { $tlsContext = null; if (isset($options[RequestOptions::CERT])) { $tlsContext ??= new ClientTlsContext(); @@ -140,6 +141,12 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte if ($tlsContext) { $connectContext = $connectContext->withTlsContext($tlsContext); } + if (isset($options[RequestOptions::FORCE_IP_RESOLVE])) { + $connectContext->withDnsTypeRestriction(match ($options[RequestOptions::FORCE_IP_RESOLVE]) { + 'v4' => DnsRecord::A, + 'v6' => DnsRecord::AAAA, + }); + } $client = (new HttpClientBuilder) ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(connector: $connector, connectContext: $connectContext))) From e642ea16e0592e22fe7bf96a5a81c645c868c280 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 3 Aug 2023 14:09:12 +0200 Subject: [PATCH 11/14] Improve curl --- src/AmpHandler.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index 22b720d..ef82222 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -64,6 +64,9 @@ public function createResponse(int $code = 200, string $reasonPhrase = ''): Resp public function __invoke(RequestInterface $request, array $options): PromiseInterface { + if (isset($options['curl'])) { + throw new AssertionError("Cannot provide curl options when using AMP backend!"); + } $deferred = new DeferredCancellation; $cancellation = $deferred->getCancellation(); $future = async(function () use ($request, $options, $cancellation) { From 56436719aceeda26992fee7fe72131c91c43c60b Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Thu, 3 Aug 2023 20:18:33 +0200 Subject: [PATCH 12/14] Add support for sink option --- composer.json | 3 ++- src/AmpHandler.php | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index d7326b4..8a10c46 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "guzzlehttp/guzzle": "^7", "psalm/phar": "^5", "leproxy/leproxy": "^0.2.2", - "revolt/event-loop-adapter-react": "^1" + "revolt/event-loop-adapter-react": "^1", + "amphp/file": "^3.0" }, "autoload": { "psr-4": { diff --git a/src/AmpHandler.php b/src/AmpHandler.php index ef82222..8e6a074 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -5,6 +5,7 @@ use Amp\CancelledException; use Amp\DeferredCancellation; use Amp\Dns\DnsRecord; +use Amp\File\File; use Amp\Http\Client\Connection\DefaultConnectionFactory; use Amp\Http\Client\Connection\UnlimitedConnectionPool; use Amp\Http\Client\HttpClient; @@ -30,7 +31,9 @@ use Throwable; use function Amp\async; +use function Amp\ByteStream\pipe; use function Amp\delay; +use function Amp\File\openFile; /** * Handler for guzzle which uses amphp/http-client. @@ -158,12 +161,21 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte if (isset($options['amp']['protocols'])) { $request->setProtocolVersions($options['amp']['protocols']); } - return self::$psrAdapter->toPsrResponse( - $client->request( - $request, - $cancellation - ) + $response = $client->request( + $request, + $cancellation ); + if (isset($options[RequestOptions::SINK])) { + if (!\is_string($options[RequestOptions::SINK])) { + throw new AssertionError("Only a file name can be provided as sink!"); + } + if (!\interface_exists(File::class)) { + throw new AssertionError("Please require amphp/file to use the sink option!"); + } + $f = openFile($options[RequestOptions::SINK], 'w'); + pipe($response->getBody(), $f, $cancellation); + } + return self::$psrAdapter->toPsrResponse($response); }); $future->ignore(); $promise = new Promise(function () use ($future, $cancellation, &$promise) { From 62d56005219be38e3922d3db7a0ec5df122bb9bb Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 4 Aug 2023 13:54:47 +0200 Subject: [PATCH 13/14] Cache all custom client instances --- src/AmpHandler.php | 119 +++++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index 8e6a074..107731d 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -42,6 +42,8 @@ final class AmpHandler { private static ?PsrAdapter $psrAdapter; private readonly HttpClient $client; + /** @var array */ + private array $cachedClients = []; public function __construct(?HttpClient $client = null) { if (!\interface_exists(PromiseInterface::class)) { @@ -94,69 +96,80 @@ public function __invoke(RequestInterface $request, array $options): PromiseInte ) || isset($options[RequestOptions::FORCE_IP_RESOLVE]) ) { - $tlsContext = null; - if (isset($options[RequestOptions::CERT])) { - $tlsContext ??= new ClientTlsContext(); - if (\is_string($options[RequestOptions::CERT])) { - $tlsContext = $tlsContext->withCertificate(new Certificate( - $options[RequestOptions::CERT], - $options[RequestOptions::SSL_KEY] ?? null, - )); - } else { - $tlsContext = $tlsContext->withCertificate(new Certificate( - $options[RequestOptions::CERT][0], - $options[RequestOptions::SSL_KEY] ?? null, - $options[RequestOptions::CERT][1] - )); - } + $cacheKey = []; + foreach ([RequestOptions::FORCE_IP_RESOLVE, RequestOptions::VERIFY, RequestOptions::PROXY, RequestOptions::CERT] as $k) { + $cacheKey[$k] = $options[$k] ?? null; } - if (isset($options[RequestOptions::VERIFY])) { - $tlsContext ??= new ClientTlsContext(); - if ($options[RequestOptions::VERIFY] === false) { - $tlsContext = $tlsContext->withoutPeerVerification(); - } elseif (\is_string($options[RequestOptions::VERIFY])) { - $tlsContext = $tlsContext->withCaFile($options[RequestOptions::VERIFY]); + $cacheKey = json_encode($cacheKey); + if (isset($this->cachedClients[$cacheKey])) { + $client = $this->cachedClients[$cacheKey]; + } else { + $tlsContext = null; + if (isset($options[RequestOptions::CERT])) { + $tlsContext ??= new ClientTlsContext(); + if (\is_string($options[RequestOptions::CERT])) { + $tlsContext = $tlsContext->withCertificate(new Certificate( + $options[RequestOptions::CERT], + $options[RequestOptions::SSL_KEY] ?? null, + )); + } else { + $tlsContext = $tlsContext->withCertificate(new Certificate( + $options[RequestOptions::CERT][0], + $options[RequestOptions::SSL_KEY] ?? null, + $options[RequestOptions::CERT][1] + )); + } + } + if (isset($options[RequestOptions::VERIFY])) { + $tlsContext ??= new ClientTlsContext(); + if ($options[RequestOptions::VERIFY] === false) { + $tlsContext = $tlsContext->withoutPeerVerification(); + } elseif (\is_string($options[RequestOptions::VERIFY])) { + $tlsContext = $tlsContext->withCaFile($options[RequestOptions::VERIFY]); + } } - } - $connector = null; - if (isset($options[RequestOptions::PROXY])) { - if (!\is_array($options['proxy'])) { - $connector = $options['proxy']; - } else { - $scheme = $request->getUri()->getScheme(); - if (isset($options['proxy'][$scheme])) { - $host = $request->getUri()->getHost(); - if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) { - $connector = $options['proxy'][$scheme]; + $connector = null; + if (isset($options[RequestOptions::PROXY])) { + if (!\is_array($options['proxy'])) { + $connector = $options['proxy']; + } else { + $scheme = $request->getUri()->getScheme(); + if (isset($options['proxy'][$scheme])) { + $host = $request->getUri()->getHost(); + if (!isset($options['proxy']['no']) || !Utils::isHostInNoProxy($host, $options['proxy']['no'])) { + $connector = $options['proxy'][$scheme]; + } } } + + if ($connector !== null) { + $connector = new Uri($connector); + $connector = match ($connector->getScheme()) { + 'http' => new Http1TunnelConnector($connector->getHost().':'.$connector->getPort()), + 'https' => new Https1TunnelConnector($connector->getHost().':'.$connector->getPort(), new ClientTlsContext($connector->getHost())), + 'socks5' => new Socks5TunnelConnector($connector->getHost().':'.$connector->getPort()) + }; + } } - if ($connector !== null) { - $connector = new Uri($connector); - $connector = match ($connector->getScheme()) { - 'http' => new Http1TunnelConnector($connector->getHost().':'.$connector->getPort()), - 'https' => new Https1TunnelConnector($connector->getHost().':'.$connector->getPort(), new ClientTlsContext($connector->getHost())), - 'socks5' => new Socks5TunnelConnector($connector->getHost().':'.$connector->getPort()) - }; + $connectContext = new ConnectContext; + if ($tlsContext) { + $connectContext = $connectContext->withTlsContext($tlsContext); + } + if (isset($options[RequestOptions::FORCE_IP_RESOLVE])) { + $connectContext->withDnsTypeRestriction(match ($options[RequestOptions::FORCE_IP_RESOLVE]) { + 'v4' => DnsRecord::A, + 'v6' => DnsRecord::AAAA, + }); } - } - $connectContext = new ConnectContext; - if ($tlsContext) { - $connectContext = $connectContext->withTlsContext($tlsContext); - } - if (isset($options[RequestOptions::FORCE_IP_RESOLVE])) { - $connectContext->withDnsTypeRestriction(match ($options[RequestOptions::FORCE_IP_RESOLVE]) { - 'v4' => DnsRecord::A, - 'v6' => DnsRecord::AAAA, - }); - } + $client = (new HttpClientBuilder) + ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(connector: $connector, connectContext: $connectContext))) + ->build(); - $client = (new HttpClientBuilder) - ->usingPool(new UnlimitedConnectionPool(new DefaultConnectionFactory(connector: $connector, connectContext: $connectContext))) - ->build(); + $this->cachedClients[$cacheKey] = $client; + } } if (isset($options['amp']['protocols'])) { $request->setProtocolVersions($options['amp']['protocols']); From 775de5bb4aa2c472b83fda680c56fcab49365c4d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 4 Aug 2023 15:14:36 +0200 Subject: [PATCH 14/14] Do not complain about curl options for now --- src/AmpHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AmpHandler.php b/src/AmpHandler.php index 107731d..5aff441 100644 --- a/src/AmpHandler.php +++ b/src/AmpHandler.php @@ -70,7 +70,7 @@ public function createResponse(int $code = 200, string $reasonPhrase = ''): Resp public function __invoke(RequestInterface $request, array $options): PromiseInterface { if (isset($options['curl'])) { - throw new AssertionError("Cannot provide curl options when using AMP backend!"); + //throw new AssertionError("Cannot provide curl options when using AMP backend!"); } $deferred = new DeferredCancellation; $cancellation = $deferred->getCancellation();