Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 2 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
needs:
- supported-versions-matrix
strategy:
fail-fast: false
matrix:
php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }}

Expand Down Expand Up @@ -56,6 +57,7 @@ jobs:
needs:
- supported-versions-matrix
strategy:
fail-fast: false
matrix:
php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }}

Expand All @@ -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
8 changes: 6 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
42 changes: 34 additions & 8 deletions src/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<string, mixed>, stream_context_param: array<string, mixed>, ssl: ?boolean, write_buffer_size: int, ssl_method: int}
* @var array{remote_socket: string|null, timeout: int, stream_context: resource, stream_context_options: array<string, mixed>, stream_context_param: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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
Expand Down Expand Up @@ -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);
}
Expand All @@ -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;
Expand All @@ -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())) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

took me quite a while to figure out my tests locally failed because the ssl certificates that we generate for the tests have been outdated.

$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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param?: array<string, mixed>, 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<string, mixed>, stream_context_param: array<string, mixed>, ssl: ?boolean, write_buffer_size: int, ssl_method: int}
* @return array{remote_socket: string|null, timeout: int, stream_context: resource, stream_context_options: array<string, mixed>, stream_context_param: array<string, mixed>, ssl: ?bool, write_buffer_size: int, ssl_method: int}
*/
protected function configure(array $config = [])
{
Expand All @@ -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;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/ResponseReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this key is always present, and phpstan complains about the unnecessary check.

throw new TimeoutException('Error while reading response, stream timed out', $request, null);
}
$header = array_shift($headers);
Expand Down
22 changes: 10 additions & 12 deletions tests/SocketHttpClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -112,7 +110,7 @@ public function testSslConnection()
]);
$response = $client->get('/', []);

$this->assertEquals(200, $response->getStatusCode());
$this->assertSame(200, $response->getStatusCode());
}

public function testSslConnectionWithClientCertificate(): void
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/server/ssl/file.srl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
34
3A
5 changes: 4 additions & 1 deletion tests/server/tcp-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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", <<<EOR
fwrite($client, str_replace(
"\n",
"\r\n",
<<<EOR
HTTP/1.1 200 OK
Content-Type: text/plain

Expand Down
14 changes: 10 additions & 4 deletions tests/server/tcp-ssl-server-client.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,27 @@
// Verify client certificate
$name = null;

if (isset(stream_context_get_options($context)['ssl']['peer_certificate'])) {
$client_cert = stream_context_get_options($context)['ssl']['peer_certificate'];
if (isset(stream_context_get_options($client)['ssl']['peer_certificate'])) {
$client_cert = stream_context_get_options($client)['ssl']['peer_certificate'];
$name = openssl_x509_parse($client_cert)['subject']['CN'];
}

if ('socket-adapter-client' == $name) {
fwrite($client, str_replace("\n", "\r\n", <<<EOR
fwrite($client, str_replace(
"\n",
"\r\n",
<<<EOR
HTTP/1.1 200 OK
Content-Type: text/plain

Test
EOR
));
} else {
fwrite($client, str_replace("\n", "\r\n", <<<EOR
fwrite($client, str_replace(
"\n",
"\r\n",
<<<EOR
HTTP/1.1 403 Invalid ssl certificate
Content-Type: text/plain

Expand Down
10 changes: 8 additions & 2 deletions tests/server/tcp-ssl-server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@
$client = stream_socket_accept($socketServer);
stream_set_blocking($client, true);
if (@stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLSv1_2_SERVER)) {
fwrite($client, str_replace("\n", "\r\n", <<<EOR
fwrite($client, str_replace(
"\n",
"\r\n",
<<<EOR
HTTP/1.1 200 OK
Content-Type: text/plain

Test
EOR
));
} else {
fwrite($client, str_replace("\n", "\r\n", <<<EOR
fwrite($client, str_replace(
"\n",
"\r\n",
<<<EOR
HTTP/1.1 400 Bad Request
Content-Type: text/plain

Expand Down