From 8732f55ed4989257f13c16eb8f8500f7e18684e8 Mon Sep 17 00:00:00 2001 From: Bilge Date: Thu, 6 Feb 2020 09:06:14 +0000 Subject: [PATCH 1/2] Upgraded Artax 3 -> HttpClient 4. --- composer.json | 6 ++---- src/AsyncHttpConnector.php | 32 +++++++++++++++----------------- src/HttpResponse.php | 12 ++++++------ 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index e8ee2f4..48faba4 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ ], "license": "LGPL-3.0", "require": { - "amphp/http-client": "^3", + "amphp/http-client": "^4", + "amphp/http-client-cookies": "^1", "connectors/ssl": "^2", "phptype/string": "^1", "scriptfusion/porter": "^5" @@ -35,9 +36,6 @@ "test": "phpunit -c test" }, "config": { - "platform": { - "php": "7.1.99" - }, "sort-packages": true } } diff --git a/src/AsyncHttpConnector.php b/src/AsyncHttpConnector.php index 62d8c2e..d7c6654 100644 --- a/src/AsyncHttpConnector.php +++ b/src/AsyncHttpConnector.php @@ -3,17 +3,15 @@ namespace ScriptFUSION\Porter\Net\Http; -use Amp\Artax\Cookie\ArrayCookieJar; -use Amp\Artax\Cookie\CookieJar; -use Amp\Artax\DefaultClient; -use Amp\Artax\DnsException; -use Amp\Artax\Request; -use Amp\Artax\Response; -use Amp\Artax\SocketException; -use Amp\Artax\TimeoutException; use Amp\ByteStream\StreamException; +use Amp\Dns\DnsException; +use Amp\Http\Client\Cookie\CookieJar; +use Amp\Http\Client\Cookie\InMemoryCookieJar; +use Amp\Http\Client\Request; +use Amp\Http\Client\Response; +use Amp\Http\Client\SocketException; +use Amp\Http\Client\TimeoutException; use Amp\Promise; -use Amp\Socket\CryptoException; use ScriptFUSION\Porter\Connector\AsyncConnector; use ScriptFUSION\Porter\Connector\AsyncDataSource; use function Amp\call; @@ -27,7 +25,7 @@ class AsyncHttpConnector implements AsyncConnector public function __construct(AsyncHttpOptions $options = null, CookieJar $cookieJar = null) { $this->options = $options ?: new AsyncHttpOptions; - $this->cookieJar = $cookieJar ?: new ArrayCookieJar; + $this->cookieJar = $cookieJar ?: new InMemoryCookieJar; } public function __clone() @@ -50,13 +48,13 @@ public function fetchAsync(AsyncDataSource $source): Promise /** @var Response $response */ $response = yield $client->request($this->createRequest($source)); $body = yield $response->getBody(); - // Retry HTTP timeouts, socket timeouts, DNS resolution, crypto negotiation and connection reset errors. - } catch (TimeoutException|SocketException|DnsException|CryptoException|StreamException $exception) { + // Retry HTTP timeouts, socket timeouts, DNS resolution and connection reset errors. + } catch (TimeoutException|SocketException|DnsException|StreamException $exception) { // Convert exception to recoverable exception. throw new HttpConnectionException($exception->getMessage(), $exception->getCode(), $exception); } - $response = HttpResponse::fromArtaxResponse($response, $body); + $response = HttpResponse::fromAmpResponse($response, $body); $code = $response->getStatusCode(); if ($code < 200 || $code >= 400) { @@ -73,10 +71,10 @@ public function fetchAsync(AsyncDataSource $source): Promise private function createRequest(AsyncHttpDataSource $source): Request { - return (new Request($source->getUrl(), $source->getMethod())) - ->withBody($source->getBody()) - ->withHeaders($source->getHeaders()) - ; + $request = new Request($source->getUrl(), $source->getMethod(), $source->getBody()); + $request->setHeaders($source->getHeaders()); + + return $request; } public function getOptions(): AsyncHttpOptions diff --git a/src/HttpResponse.php b/src/HttpResponse.php index f804719..7dd9486 100644 --- a/src/HttpResponse.php +++ b/src/HttpResponse.php @@ -3,7 +3,7 @@ namespace ScriptFUSION\Porter\Net\Http; -use Amp\Artax\Response; +use Amp\Http\Client\Response; use ScriptFUSION\Type\StringType; /** @@ -38,14 +38,14 @@ public static function fromPhpWrapper(array $headers, string $body = null): self return $response; } - public static function fromArtaxResponse(Response $artax, string $body): self + public static function fromAmpResponse(Response $ampResponse, string $body): self { $response = new self; - $response->headers = $artax->getHeaders(); - $response->version = $artax->getProtocolVersion(); - $response->statusCode = $artax->getStatus(); - $response->statusPhrase = $artax->getReason(); + $response->headers = $ampResponse->getHeaders(); + $response->version = $ampResponse->getProtocolVersion(); + $response->statusCode = $ampResponse->getStatus(); + $response->statusPhrase = $ampResponse->getReason(); $response->body = $body; return $response; From ccd35417ae136ae876a6a2237761d70272eec801 Mon Sep 17 00:00:00 2001 From: Bilge Date: Sun, 15 Nov 2020 23:12:16 +0000 Subject: [PATCH 2/2] Completed Artax 3 -> HttpClient 4 upgrade. Removed PHP 7.1 support (not supported by http-client-cookies). --- .travis.yml | 5 +--- composer.json | 6 ++--- src/AsyncHttpConnector.php | 35 ++++++++++++++++++++++----- src/AsyncHttpDataSource.php | 2 +- src/AsyncHttpOptions.php | 23 +++++------------- test/Functional/HttpConnectorTest.php | 22 +++++++++++++---- test/Functional/servers/feedback.php | 4 +++ test/Unit/AsyncHttpDataSourceTest.php | 2 +- 8 files changed, 62 insertions(+), 37 deletions(-) diff --git a/.travis.yml b/.travis.yml index de3e58a..8853efd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ addons: language: php php: - - 7.1 - 7.2 - 7.3 - 7.4 @@ -33,9 +32,7 @@ install: - composer --no-interaction update --no-progress --no-suggest $DEPENDENCIES script: - - composer test -- --coverage-clover=build/logs/clover.xml --coverage-xml=build/coverage/coverage-xml - --log-junit=build/coverage/phpunit.junit.xml || exit - - travis_retry timeout -k 60s 30s vendor/bin/infection --min-msi=69 --threads=$(nproc) --coverage=build/coverage + - composer test -- --coverage-clover=build/logs/clover.xml after_success: - composer require php-coveralls/php-coveralls:^2 diff --git a/composer.json b/composer.json index 48faba4..e2fdb38 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ ], "license": "LGPL-3.0", "require": { - "amphp/http-client": "^4", + "php": "^7.2", + "amphp/http-client": "^4.1", "amphp/http-client-cookies": "^1", "connectors/ssl": "^2", "phptype/string": "^1", @@ -17,9 +18,8 @@ }, "require-dev": { "amphp/phpunit-util": "^1.2", - "infection/infection": ">=0.13.6", "mockery/mockery": "^1", - "phpunit/phpunit": "^7.1.3", + "phpunit/phpunit": "^7.5", "symfony/process": "^3.3" }, "autoload": { diff --git a/src/AsyncHttpConnector.php b/src/AsyncHttpConnector.php index d7c6654..5375d95 100644 --- a/src/AsyncHttpConnector.php +++ b/src/AsyncHttpConnector.php @@ -5,13 +5,18 @@ use Amp\ByteStream\StreamException; use Amp\Dns\DnsException; +use Amp\Http\Client\Connection\UnlimitedConnectionPool; +use Amp\Http\Client\Cookie\CookieInterceptor; use Amp\Http\Client\Cookie\CookieJar; use Amp\Http\Client\Cookie\InMemoryCookieJar; +use Amp\Http\Client\HttpClient; +use Amp\Http\Client\HttpClientBuilder; use Amp\Http\Client\Request; use Amp\Http\Client\Response; use Amp\Http\Client\SocketException; use Amp\Http\Client\TimeoutException; use Amp\Promise; +use Amp\Socket\TlsException; use ScriptFUSION\Porter\Connector\AsyncConnector; use ScriptFUSION\Porter\Connector\AsyncDataSource; use function Amp\call; @@ -22,16 +27,20 @@ class AsyncHttpConnector implements AsyncConnector private $cookieJar; + private $pool; + public function __construct(AsyncHttpOptions $options = null, CookieJar $cookieJar = null) { $this->options = $options ?: new AsyncHttpOptions; $this->cookieJar = $cookieJar ?: new InMemoryCookieJar; + $this->pool = new UnlimitedConnectionPool(); } public function __clone() { $this->options = clone $this->options; $this->cookieJar = clone $this->cookieJar; + // Sharing the pool is intended and should be harmless. } public function fetchAsync(AsyncDataSource $source): Promise @@ -41,15 +50,14 @@ public function fetchAsync(AsyncDataSource $source): Promise throw new \InvalidArgumentException('Source must be of type: AsyncHttpDataSource.'); } - $client = new DefaultClient($this->cookieJar); - $client->setOptions($this->options->extractArtaxOptions()); + $client = $this->createClient(); try { /** @var Response $response */ $response = yield $client->request($this->createRequest($source)); - $body = yield $response->getBody(); - // Retry HTTP timeouts, socket timeouts, DNS resolution and connection reset errors. - } catch (TimeoutException|SocketException|DnsException|StreamException $exception) { + $body = yield $response->getBody()->buffer(); + // Retry HTTP timeouts, socket timeouts, DNS resolution, TLS negotiation and connection reset errors. + } catch (TimeoutException|SocketException|DnsException|TlsException|StreamException $exception) { // Convert exception to recoverable exception. throw new HttpConnectionException($exception->getMessage(), $exception->getCode(), $exception); } @@ -69,10 +77,25 @@ public function fetchAsync(AsyncDataSource $source): Promise }); } + private function createClient(): HttpClient + { + return (new HttpClientBuilder()) + ->usingPool($this->pool) + ->interceptNetwork(new CookieInterceptor($this->cookieJar)) + ->followRedirects($this->options->getMaxRedirects()) + // We have our own retry implementation. + ->retry(0) + ->build() + ; + } + private function createRequest(AsyncHttpDataSource $source): Request { - $request = new Request($source->getUrl(), $source->getMethod(), $source->getBody()); + $request = new Request($source->getUrl(), $source->getMethod()); + $request->setBody($source->getBody()); $request->setHeaders($source->getHeaders()); + $request->setTransferTimeout($this->options->getTransferTimeout()); + $request->setBodySizeLimit($this->options->getMaxBodyLength()); return $request; } diff --git a/src/AsyncHttpDataSource.php b/src/AsyncHttpDataSource.php index b4c0c4e..ca1c8a9 100644 --- a/src/AsyncHttpDataSource.php +++ b/src/AsyncHttpDataSource.php @@ -3,7 +3,7 @@ namespace ScriptFUSION\Porter\Net\Http; -use Amp\Artax\RequestBody; +use Amp\Http\Client\RequestBody; use Amp\Promise; use ScriptFUSION\Porter\Connector\AsyncDataSource; use function Amp\call; diff --git a/src/AsyncHttpOptions.php b/src/AsyncHttpOptions.php index 686ef3a..83f6998 100644 --- a/src/AsyncHttpOptions.php +++ b/src/AsyncHttpOptions.php @@ -3,8 +3,6 @@ namespace ScriptFUSION\Porter\Net\Http; -use Amp\Artax\Client; - /** * Encapsulates async HTTP client options. */ @@ -16,8 +14,8 @@ final class AsyncHttpOptions // Number of redirects to follow, or 0 to disable redirects. private $maxRedirects = 5; - // Automatically add a "Referer" header on redirect. - private $autoReferrer = true; + // Maximum body length in bytes. Default 10MiB. + private $maxBodyLength = 0x100000 * 10; public function getTransferTimeout(): int { @@ -43,24 +41,15 @@ public function setMaxRedirects(int $maxRedirects): self return $this; } - public function getAutoReferrer(): bool + public function getMaxBodyLength(): int { - return $this->autoReferrer; + return $this->maxBodyLength; } - public function setAutoReferrer(bool $autoReferer): self + public function setMaxBodyLength($maxBodyLength): self { - $this->autoReferrer = $autoReferer; + $this->maxBodyLength = $maxBodyLength; return $this; } - - public function extractArtaxOptions(): array - { - return [ - Client::OP_AUTO_REFERER => $this->autoReferrer, - Client::OP_MAX_REDIRECTS => $this->maxRedirects, - Client::OP_TRANSFER_TIMEOUT => $this->transferTimeout, - ]; - } } diff --git a/test/Functional/HttpConnectorTest.php b/test/Functional/HttpConnectorTest.php index f42f9b2..b06354a 100644 --- a/test/Functional/HttpConnectorTest.php +++ b/test/Functional/HttpConnectorTest.php @@ -3,7 +3,9 @@ namespace ScriptFUSIONTest\Functional\Porter\Net\Http; -use Amp\Artax\StringBody; +use Amp\Http\Client\Body\StringBody; +use Amp\Http\Cookie\CookieAttributes; +use Amp\Http\Cookie\ResponseCookie; use PHPUnit\Framework\TestCase; use ScriptFUSION\Porter\Connector\AsyncDataSource; use ScriptFUSION\Porter\Connector\Connector; @@ -89,6 +91,15 @@ public function testAsyncConnectionToLocalWebserver(): void $this->connector = new AsyncHttpConnector; + // Test cookies are sent. + $this->connector->getCookieJar()->store( + new ResponseCookie( + $cookieName = uniqid(), + $cookievalue = 'Alfa', + CookieAttributes::default()->withDomain(explode(':', self::HOST)[0]) + ) + ); + try { $response = $this->fetchAsync( self::buildAsyncDataSource() @@ -106,8 +117,9 @@ public function testAsyncConnectionToLocalWebserver(): void self::assertSame('1.1', $response->getProtocolVersion()); self::assertTrue($response->hasHeader('x-powered-by')); self::assertRegExp('[\APOST \Q' . self::HOST . '/' . self::URI . '\E HTTP/\d+\.\d+$]m', $response->getBody()); - self::assertRegExp("[^$headerName: $headerValue$]m", $response->getBody()); - self::assertStringEndsWith("\n\n$body", $response->getBody()); + self::assertRegExp("[^$headerName: $headerValue$]m", $response->getBody(), 'Headers sent.'); + self::assertRegExp("[^Cookie: \Q$cookieName=$cookievalue\E$]m", $response->getBody(), 'Cookies sent.'); + self::assertStringEndsWith("\n\n$body", $response->getBody(), 'Body sent.'); } public function testConnectionTimeout(): void @@ -126,8 +138,8 @@ public function testErrorResponse(): void try { $this->fetch(self::buildDataSource('404.php')); } catch (HttpServerException $exception) { - $this->assertStringEndsWith('foo', $exception->getMessage()); - $this->assertSame('foo', $exception->getResponse()->getBody()); + self::assertStringEndsWith('foo', $exception->getMessage()); + self::assertSame('foo', $exception->getResponse()->getBody()); throw $exception; } finally { diff --git a/test/Functional/servers/feedback.php b/test/Functional/servers/feedback.php index 6e8439f..dc20993 100644 --- a/test/Functional/servers/feedback.php +++ b/test/Functional/servers/feedback.php @@ -5,4 +5,8 @@ echo "$name: $value\n"; } +foreach ($_COOKIE as $name => $value) { + echo "Cookie: $name=$value\n"; +} + echo "\n", file_get_contents('php://input'); diff --git a/test/Unit/AsyncHttpDataSourceTest.php b/test/Unit/AsyncHttpDataSourceTest.php index 5ea5680..ea6d4aa 100644 --- a/test/Unit/AsyncHttpDataSourceTest.php +++ b/test/Unit/AsyncHttpDataSourceTest.php @@ -3,7 +3,7 @@ namespace ScriptFUSIONTest\Unit; -use Amp\Artax\StringBody; +use Amp\Http\Client\Body\StringBody; use Amp\PHPUnit\AsyncTestCase; use ScriptFUSION\Porter\Net\Http\AsyncHttpDataSource;