diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1231aa2..5208683 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,6 +24,7 @@ jobs: needs: - supported-versions-matrix strategy: + fail-fast: false matrix: php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} @@ -56,6 +57,7 @@ jobs: needs: - supported-versions-matrix strategy: + fail-fast: false matrix: php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} @@ -81,30 +83,3 @@ jobs: - name: Execute tests run: composer test - - coverage: - name: Code Coverage - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: 8.3 - tools: composer - coverage: xdebug - - - name: Install dependencies - run: composer update --prefer-dist --no-interaction --no-progress - - - name: generate ssl - run: cd ./tests/server/ssl && ./generate.sh && pwd && ls -la && cd ../../../ - - - name: boot test server - run: vendor/bin/http_test_server > /dev/null 2>&1 & - - - name: Execute tests - run: composer test-ci diff --git a/composer.json b/composer.json index 0689754..cc37ab7 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,17 @@ "symfony/options-resolver": "^2.6 || ^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { + "ext-openssl": "*", "friendsofphp/php-cs-fixer": "^3.51", - "php-http/client-integration-tests": "^3.1.1", + "php-http/client-integration-tests": "^4.0", "php-http/message": "^1.16", "php-http/client-common": "^2.7", - "phpunit/phpunit": "^8.5.23 || ~9.5", + "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0", "php-http/message-factory": "^1.1" }, + "conflict": { + "guzzlehttp/psr7": "<2.0" + }, "provide": { "php-http/client-implementation": "1.0", "psr/http-client-implementation": "1.0" diff --git a/src/Client.php b/src/Client.php index 116b621..d3cd14a 100644 --- a/src/Client.php +++ b/src/Client.php @@ -10,6 +10,7 @@ use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -28,16 +29,16 @@ class Client implements HttpClient use ResponseReader; /** - * @var array{remote_socket: string|null, timeout: int, stream_context: resource, stream_context_options: array, stream_context_param: array, ssl: ?boolean, write_buffer_size: int, ssl_method: int} + * @var array{remote_socket: string|null, timeout: int, stream_context: resource, stream_context_options: array, stream_context_param: array, ssl: ?bool, write_buffer_size: int, ssl_method: int} */ private $config; /** * Constructor. * - * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?boolean, write_buffer_size?: int, ssl_method?: int}|ResponseFactoryInterface $config1 - * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?boolean, write_buffer_size?: int, ssl_method?: int}|null $config2 Mistake when refactoring the constructor from version 1 to version 2 - used as $config if set and $configOrResponseFactory is a response factory instance - * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?boolean, write_buffer_size?: int, ssl_method?: int} $config intended for version 1 BC, used as $config if $config2 is not set and $configOrResponseFactory is a response factory instance + * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?bool, write_buffer_size?: int, ssl_method?: int}|ResponseFactoryInterface $config1 + * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?bool, write_buffer_size?: int, ssl_method?: int}|null $config2 Mistake when refactoring the constructor from version 1 to version 2 - used as $config if set and $configOrResponseFactory is a response factory instance + * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?bool, write_buffer_size?: int, ssl_method?: int} $config intended for version 1 BC, used as $config if $config2 is not set and $configOrResponseFactory is a response factory instance * * string|null remote_socket Remote entrypoint (can be a tcp or unix domain address) * int timeout Timeout before canceling request @@ -110,6 +111,7 @@ protected function createSocket(RequestInterface $request, string $remote, bool $socket = @stream_socket_client($remote, $errNo, $errMsg, floor($this->config['timeout'] / 1000), STREAM_CLIENT_CONNECT, $this->config['stream_context']); if (false === $socket) { + $errMsg = $errMsg ?: '[no message set]'; if (110 === $errNo) { throw new TimeoutException($errMsg, $request); } @@ -120,7 +122,13 @@ protected function createSocket(RequestInterface $request, string $remote, bool stream_set_timeout($socket, (int) floor($this->config['timeout'] / 1000), $this->config['timeout'] % 1000); if ($useSsl && false === @stream_socket_enable_crypto($socket, true, $this->config['ssl_method'])) { - throw new SSLConnectionException(sprintf('Cannot enable tls: %s', error_get_last()['message'] ?? 'no error reported'), $request); + $errorMessage = error_get_last()['message'] ?? 'no error reported'; + $opensslErrors = $this->collectOpenSslErrors(); + if ('' !== $opensslErrors) { + $errorMessage .= '; '.$opensslErrors; + } + + throw new SSLConnectionException(sprintf('Cannot enable tls method %s: %s', $this->config['ssl_method'], $errorMessage), $request); } return $socket; @@ -138,12 +146,25 @@ protected function closeSocket($socket) fclose($socket); } + /** + * Collect and format OpenSSL error queue entries, if available. + */ + private function collectOpenSslErrors(): string + { + $errors = []; + while (false !== ($error = openssl_error_string())) { + $errors[] = $error; + } + + return implode(' | ', $errors); + } + /** * Return configuration for the socket client. * - * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?boolean, write_buffer_size?: int, ssl_method?: int} $config + * @param array{remote_socket?: string|null, timeout?: int, stream_context?: resource, stream_context_options?: array, stream_context_param?: array, ssl?: ?bool, write_buffer_size?: int, ssl_method?: int} $config * - * @return array{remote_socket: string|null, timeout: int, stream_context: resource, stream_context_options: array, stream_context_param: array, ssl: ?boolean, write_buffer_size: int, ssl_method: int} + * @return array{remote_socket: string|null, timeout: int, stream_context: resource, stream_context_options: array, stream_context_param: array, ssl: ?bool, write_buffer_size: int, ssl_method: int} */ protected function configure(array $config = []) { @@ -167,7 +188,12 @@ protected function configure(array $config = []) $resolver->setAllowedTypes('stream_context', 'resource'); $resolver->setAllowedTypes('ssl', ['bool', 'null']); - return $resolver->resolve($config); + $configuration = $resolver->resolve($config); + if ($configuration['ssl'] && !function_exists('openssl_error_string')) { + throw new InvalidOptionsException('You can not enable ssl when ext-openssl is not installed'); + } + + return $configuration; } /** diff --git a/src/ResponseReader.php b/src/ResponseReader.php index d5beae6..7dc5c59 100644 --- a/src/ResponseReader.php +++ b/src/ResponseReader.php @@ -39,7 +39,7 @@ protected function readResponse(RequestInterface $request, $socket): ResponseInt $metadatas = stream_get_meta_data($socket); - if (array_key_exists('timed_out', $metadatas) && true === $metadatas['timed_out']) { + if ($metadatas['timed_out']) { throw new TimeoutException('Error while reading response, stream timed out', $request, null); } $header = array_shift($headers); diff --git a/tests/SocketHttpClientTest.php b/tests/SocketHttpClientTest.php index 657f2ff..46ace56 100644 --- a/tests/SocketHttpClientTest.php +++ b/tests/SocketHttpClientTest.php @@ -10,18 +10,18 @@ class SocketHttpClientTest extends BaseTestCase { - public function createClient($options = []) + public function createClient($options = []): HttpMethodsClient { return new HttpMethodsClient(new SocketHttpClient($options), new Psr17Factory()); } - public function testTcpSocketDomain() + public function testTcpSocketDomain(): void { $this->startServer('tcp-server'); $client = $this->createClient(['remote_socket' => '127.0.0.1:19999']); $response = $client->get('/', []); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testNoRemote(): void @@ -37,7 +37,7 @@ public function testRemoteInUri(): void $client = $this->createClient(); $response = $client->get('http://127.0.0.1:19999/', []); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testRemoteInHostHeader(): void @@ -46,8 +46,7 @@ public function testRemoteInHostHeader(): void $client = $this->createClient(); $response = $client->get('/', ['Host' => '127.0.0.1:19999']); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testBrokenSocket(): void @@ -71,10 +70,9 @@ public function testSslRemoteInUri(): void ], ], ]); - $response = $client->get('/', []); + $response = $client->get('/'); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testUnixSocketDomain(): void @@ -86,7 +84,7 @@ public function testUnixSocketDomain(): void ]); $response = $client->get('/', []); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testNetworkExceptionOnConnectError(): void @@ -112,7 +110,7 @@ public function testSslConnection() ]); $response = $client->get('/', []); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testSslConnectionWithClientCertificate(): void @@ -132,7 +130,7 @@ public function testSslConnectionWithClientCertificate(): void ]); $response = $client->get('/', []); - $this->assertEquals(200, $response->getStatusCode()); + $this->assertSame(200, $response->getStatusCode()); } public function testInvalidSslConnectionWithClientCertificate(): void diff --git a/tests/server/ssl/file.srl b/tests/server/ssl/file.srl index a787364..44442d2 100644 --- a/tests/server/ssl/file.srl +++ b/tests/server/ssl/file.srl @@ -1 +1 @@ -34 +3A diff --git a/tests/server/tcp-server.php b/tests/server/tcp-server.php index 4929984..dfe9ce5 100644 --- a/tests/server/tcp-server.php +++ b/tests/server/tcp-server.php @@ -5,7 +5,10 @@ $socketServer = stream_socket_server('127.0.0.1:19999'); $client = stream_socket_accept($socketServer); -fwrite($client, str_replace("\n", "\r\n", <<