diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..fb288d9 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing + +Please see our [contributing guide](http://docs.php-http.org/en/latest/development/contributing.html). diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..d4ecf20 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,27 @@ +| Q | A +| ------------ | --- +| Bug? | no|yes +| New Feature? | no|yes +| Version | Specific version or SHA of a commit + + +#### Actual Behavior + +What is the actual behavior? + + +#### Expected Behavior + +What is the behavior you expect? + + +#### Steps to Reproduce + +What are the steps to reproduce this bug? Please add code examples, +screenshots or links to GitHub repositories that reproduce the problem. + + +#### Possible Solutions + +If you have already ideas how to solve the issue, add them here. +(remove this section if not needed) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..323987b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,43 @@ +| Q | A +| --------------- | --- +| Bug fix? | no|yes +| New feature? | no|yes +| BC breaks? | no|yes +| Deprecations? | no|yes +| Related tickets | fixes #X, partially #Y, mentioned in #Z +| Documentation | if this is a new feature, link to pull request in https://github.com/php-http/documentation that adds relevant documentation +| License | MIT + + +#### What's in this PR? + +Explain what the changes in this PR do. + + +#### Why? + +Which problem does the PR fix? (remove this section if you linked an issue above) + + +#### Example Usage + +``` php +// If you added new features, show examples of how to use them here +// (remove this section if not a new feature) + +$foo = new Foo(); + +// Now we can do +$foo->doSomething(); +``` + + +#### Checklist + +- [ ] Updated CHANGELOG.md to describe BC breaks / deprecations | new feature | bugfix +- [ ] Documentation pull request created (if not simply a bugfix) + + +#### To Do + +- [ ] If the PR is not complete but you want to discuss the approach, list what remains to be done here diff --git a/.github/workflows/.editorconfig b/.github/workflows/.editorconfig new file mode 100644 index 0000000..7bd3346 --- /dev/null +++ b/.github/workflows/.editorconfig @@ -0,0 +1,2 @@ +[*.yml] +indent_size = 2 diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 0000000..8e3f23e --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,20 @@ +name: Checks + +on: + push: + branches: + - 2.x + pull_request: + +jobs: + composer-normalize: + name: Composer Normalize + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Composer normalize + uses: docker://ergebnis/composer-normalize-action + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1231aa2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,110 @@ +name: CI + +on: + push: + branches: + - '[0-9]+.x' + - '[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.x' + pull_request: + +jobs: + supported-versions-matrix: + name: Supported Versions Matrix + runs-on: ubuntu-latest + outputs: + version: ${{ steps.supported-versions-matrix.outputs.version }} + steps: + - uses: actions/checkout@v4 + - id: supported-versions-matrix + uses: WyriHaximus/github-action-composer-php-versions-in-range@v1 + latest: + name: PHP ${{ matrix.php }} Latest + runs-on: ubuntu-latest + needs: + - supported-versions-matrix + strategy: + matrix: + php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + coverage: none + + - 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 + + lowest: + name: PHP ${{ matrix.php }} Lowest + runs-on: ubuntu-latest + needs: + - supported-versions-matrix + strategy: + matrix: + php: ${{ fromJson(needs.supported-versions-matrix.outputs.version) }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + coverage: none + + - name: Install dependencies + run: composer update --prefer-dist --prefer-stable --prefer-lowest --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 + + 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/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..cfa74b1 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,36 @@ +name: Static analysis + +on: + push: + branches: + - 2.x + pull_request: + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + env: + REQUIRE_DEV: true + with: + args: analyze --no-progress + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: PHP-CS-Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --dry-run --diff diff --git a/.gitignore b/.gitignore index da734f1..322ef74 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ -.puli/ -build/ -vendor/ -composer.lock -phpspec.yml -phpunit.xml +/.php-cs-fixer.cache +/.puli/ +/build/ +/composer.lock +/phpstan.neon +/phpunit.xml +/tests/server/ssl/*.pem +/tests/server/ssl/*.key +/tests/server/ssl/*.req +/vendor/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..bcb9a69 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,16 @@ +exclude('vendor') + ->in(__DIR__) +; + +$config = (new PhpCsFixer\Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + ]) + ->setFinder($finder) +; + +return $config; diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 23ba165..0000000 --- a/.php_cs +++ /dev/null @@ -1,13 +0,0 @@ - /dev/null 2>&1 & - -script: - - cd ./tests/server/ssl && ./generate.sh && pwd && ls -la && cd ../../../ - - $TEST_COMMAND - - ./vendor/bin/phpunit tests/SocketClientFeatureTest.php --printer Http\\Client\\Tests\\FeatureTestListener || echo "" - -after_success: - - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 8aaef69..b766856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Change Log +## 2.3.0 + + * Fixed compatibility with `psr/http-message` v2 + * The `Http\Client\Socket\Stream` has BC breaks if you extended it. It is not meant to be extended, declaring it as `@internal` now. + +## 2.2.0 + + * Allow installation with Symfony 7 + +## 2.1.1 + + * Fixed constructor to work nicely with version 1 style arguments (e.g. HttplugBundle) + * Fixed PHP 8 compatibility for stream timeouts + * Renamed `master` branch to `2.x` for semantic branch naming. + * Add Symfony 6 compatibility + +## 2.1.0 + + * Add php 8 compatibility + +## 2.0.2 + + * Fixed composer "provide" section to say that we provide `psr/http-client-implementation` + +## 2.0.1 + + * Fix wrong call to trigger_error + +## 2.0.0 + + * Remove response and stream factory, use direct implementation of nyholm/psr7 + * PSR18 and HTTPlug 2 support + * Remove support for php 5.5, 5.6, 7.0 and 7.1 + * SSL Method now defaults to `STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT` + ## 1.4.0 * Support for Symfony 4 @@ -12,7 +47,7 @@ * `ConnectionException` * `InvalidRequestException` * `SSLConnectionException` - + ## 1.2.0 * Dropped PHP 5.4 support diff --git a/README.md b/README.md index d5620f4..27be400 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Latest Version](https://img.shields.io/github/release/php-http/socket-client.svg?style=flat-square)](https://github.com/php-http/socket-client/releases) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) -[![Build Status](https://img.shields.io/travis/php-http/socket-client.svg?branch=master&style=flat-square)](https://travis-ci.org/php-http/socket-client) +[![Build Status](https://github.com/php-http/socket-client/actions/workflows/ci.yml/badge.svg?branch=2.x)](https://github.com/php-http/socket-client/actions/workflows/ci.yml) [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/php-http/socket-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/socket-client) [![Quality Score](https://img.shields.io/scrutinizer/g/php-http/socket-client.svg?style=flat-square)](https://scrutinizer-ci.com/g/php-http/socket-client) [![Total Downloads](https://img.shields.io/packagist/dt/php-http/socket-client.svg?style=flat-square)](https://packagist.org/packages/php-http/socket-client) @@ -28,14 +28,14 @@ First launch the http server: $ ./vendor/bin/http_test_server > /dev/null 2>&1 & ``` -Then generate ssh certificates: +Then generate SSL certificates: ```bash -$ cd ./tests/server/ssl -$ ./generate.sh -$ cd ../../../ +$ composer gen-ssl ``` +Note: If you are running this on macOS and get the following error: "Error opening CA Private Key privkey.pem", check [this](ssl-macOS.md) file. + Now run the test suite: ``` bash diff --git a/composer.json b/composer.json index ab43a37..0689754 100644 --- a/composer.json +++ b/composer.json @@ -9,20 +9,24 @@ } ], "require": { - "php": "^5.5 || ^7.0", - "symfony/options-resolver": "^2.6 || ^3.0 || ^4.0", - "php-http/httplug": "^1.0", - "php-http/message-factory": "^1.0.2", - "php-http/discovery": "^1.0" + "php": "^8.1", + "nyholm/psr7": "^1.8.1", + "php-http/httplug": "^2.4", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "^2.6 || ^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0" }, "require-dev": { - "guzzlehttp/psr7": "^1.2", - "php-http/client-integration-tests": "^0.6", - "php-http/message": "^1.0", - "php-http/client-common": "^1.0" + "friendsofphp/php-cs-fixer": "^3.51", + "php-http/client-integration-tests": "^3.1.1", + "php-http/message": "^1.16", + "php-http/client-common": "^2.7", + "phpunit/phpunit": "^8.5.23 || ~9.5", + "php-http/message-factory": "^1.1" }, "provide": { - "php-http/client-implementation": "1.0" + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" }, "autoload": { "psr-4": { @@ -35,13 +39,11 @@ } }, "scripts": { + "cs-check": "vendor/bin/php-cs-fixer fix --dry-run", + "cs-fix": "vendor/bin/php-cs-fixer fix", "test": "vendor/bin/phpunit", - "test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml" - }, - "extra": { - "branch-alias": { - "dev-master": "1.1-dev" - } + "test-ci": "vendor/bin/phpunit --coverage-clover build/coverage.xml", + "gen-ssl": "tests/server/ssl/generate.sh" }, "prefer-stable": true, "minimum-stability": "dev" diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..f88ed86 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,25 @@ +parameters: + level: max + paths: + - src + ignoreErrors: + # phpstan seems confused by passing a variable by reference to stream_select + - + message: '#^Negated boolean expression is always false.$#' + count: 1 + path: src/RequestWriter.php + + - + message: "#^Method Http\\\\Client\\\\Socket\\\\Client\\:\\:configure\\(\\) should return array\\{remote_socket\\: string\\|null, timeout\\: int, stream_context\\: resource, stream_context_options\\: array\\, stream_context_param\\: array\\, ssl\\: bool\\|null, write_buffer_size\\: int, ssl_method\\: int\\} but returns array\\.$#" + count: 1 + path: src/Client.php + + - + message: "#^Parameter \\#1 \\$options of function stream_context_create expects array\\|null, mixed given\\.$#" + count: 1 + path: src/Client.php + + - + message: "#^Parameter \\#2 \\$params of function stream_context_create expects array\\|null, mixed given\\.$#" + count: 1 + path: src/Client.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2ab0582..f12146d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,17 +1,16 @@ - - - - tests/ - tests/SocketClientFeatureTest.php - - - - - - - - src/ - - + + + + src/ + + + + + tests/ + + + + + diff --git a/src/Client.php b/src/Client.php index cb84123..116b621 100644 --- a/src/Client.php +++ b/src/Client.php @@ -6,9 +6,10 @@ use Http\Client\Socket\Exception\ConnectionException; use Http\Client\Socket\Exception\InvalidRequestException; use Http\Client\Socket\Exception\SSLConnectionException; -use Http\Discovery\MessageFactoryDiscovery; -use Http\Message\ResponseFactory; +use Http\Client\Socket\Exception\TimeoutException; use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -18,51 +19,49 @@ * Use stream and socket capabilities of the core of PHP to send HTTP requests * * @author Joel Wurtz + * + * @final */ class Client implements HttpClient { use RequestWriter; use ResponseReader; - private $config = [ - 'remote_socket' => null, - 'timeout' => null, - 'stream_context_options' => [], - 'stream_context_param' => [], - 'ssl' => null, - 'write_buffer_size' => 8192, - 'ssl_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT, - ]; + /** + * @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} + */ + private $config; /** * Constructor. * - * @param ResponseFactory $responseFactory Response factory for creating response - * @param array $config { + * @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 * - * @var string $remote_socket Remote entrypoint (can be a tcp or unix domain address) - * @var int $timeout Timeout before canceling request - * @var array $stream_context_options Context options as defined in the PHP documentation - * @var array $stream_context_param Context params as defined in the PHP documentation - * @var bool $ssl Use ssl, default to scheme from request, false if not present - * @var int $write_buffer_size Buffer when writing the request body, defaults to 8192 - * @var int $ssl_method Crypto method for ssl/tls, see PHP doc, defaults to STREAM_CRYPTO_METHOD_TLS_CLIENT - * } + * string|null remote_socket Remote entrypoint (can be a tcp or unix domain address) + * int timeout Timeout before canceling request + * stream resource The initialized stream context, if not set the context is created from the options and param. + * array stream_context_options Context options as defined in the PHP documentation + * array stream_context_param Context params as defined in the PHP documentation + * boolean ssl Use ssl, default to scheme from request, false if not present + * int write_buffer_size Buffer when writing the request body, defaults to 8192 + * int ssl_method Crypto method for ssl/tls, see PHP doc, defaults to STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT */ - public function __construct(ResponseFactory $responseFactory = null, array $config = []) + public function __construct($config1 = [], $config2 = null, array $config = []) { - if (null === $responseFactory) { - $responseFactory = MessageFactoryDiscovery::find(); + if (\is_array($config1)) { + $this->config = $this->configure($config1); + + return; } - $this->responseFactory = $responseFactory; - $this->config = $this->configure($config); + @trigger_error('Passing a Psr\Http\Message\ResponseFactoryInterface to SocketClient is deprecated, and will be removed in 3.0, you should only pass config options.', E_USER_DEPRECATED); + + $this->config = $this->configure($config2 ?: $config); } - /** - * {@inheritdoc} - */ - public function sendRequest(RequestInterface $request) + public function sendRequest(RequestInterface $request): ResponseInterface { $remote = $this->config['remote_socket']; $useSsl = $this->config['ssl']; @@ -100,24 +99,28 @@ public function sendRequest(RequestInterface $request) * @param string $remote Entrypoint for the connection * @param bool $useSsl Whether to use ssl or not * - * @throws ConnectionException|SSLConnectionException When the connection fail - * * @return resource Socket resource + * + * @throws ConnectionException|SSLConnectionException When the connection fail */ - protected function createSocket(RequestInterface $request, $remote, $useSsl) + protected function createSocket(RequestInterface $request, string $remote, bool $useSsl) { $errNo = null; $errMsg = null; $socket = @stream_socket_client($remote, $errNo, $errMsg, floor($this->config['timeout'] / 1000), STREAM_CLIENT_CONNECT, $this->config['stream_context']); if (false === $socket) { + if (110 === $errNo) { + throw new TimeoutException($errMsg, $request); + } + throw new ConnectionException($errMsg, $request); } - stream_set_timeout($socket, floor($this->config['timeout'] / 1000), $this->config['timeout'] % 1000); + 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']), $request); + throw new SSLConnectionException(sprintf('Cannot enable tls: %s', error_get_last()['message'] ?? 'no error reported'), $request); } return $socket; @@ -127,6 +130,8 @@ protected function createSocket(RequestInterface $request, $remote, $useSsl) * Close the socket, used when having an error. * * @param resource $socket + * + * @return void */ protected function closeSocket($socket) { @@ -136,19 +141,26 @@ protected function closeSocket($socket) /** * Return configuration for the socket client. * - * @param array $config Configuration from user + * @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 * - * @return array Configuration resolved + * @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} */ protected function configure(array $config = []) { $resolver = new OptionsResolver(); - $resolver->setDefaults($this->config); + $resolver->setDefaults([ + 'remote_socket' => null, + 'timeout' => null, + 'stream_context_options' => [], + 'stream_context_param' => [], + 'ssl' => null, + 'write_buffer_size' => 8192, + 'ssl_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT, + ]); $resolver->setDefault('stream_context', function (Options $options) { return stream_context_create($options['stream_context_options'], $options['stream_context_param']); }); - - $resolver->setDefault('timeout', ini_get('default_socket_timeout') * 1000); + $resolver->setDefault('timeout', ((int) ini_get('default_socket_timeout')) * 1000); $resolver->setAllowedTypes('stream_context_options', 'array'); $resolver->setAllowedTypes('stream_context_param', 'array'); @@ -161,11 +173,9 @@ protected function configure(array $config = []) /** * Return remote socket from the request. * - * @param RequestInterface $request + * @return string * * @throws InvalidRequestException When no remote can be determined from the request - * - * @return string */ private function determineRemoteFromRequest(RequestInterface $request) { diff --git a/src/Exception/BrokenPipeException.php b/src/Exception/BrokenPipeException.php index 58eb1bb..4cf4b62 100644 --- a/src/Exception/BrokenPipeException.php +++ b/src/Exception/BrokenPipeException.php @@ -2,8 +2,6 @@ namespace Http\Client\Socket\Exception; -use Http\Client\Exception\NetworkException; - class BrokenPipeException extends NetworkException { } diff --git a/src/Exception/ConnectionException.php b/src/Exception/ConnectionException.php index 88dc252..67b628d 100644 --- a/src/Exception/ConnectionException.php +++ b/src/Exception/ConnectionException.php @@ -2,8 +2,6 @@ namespace Http\Client\Socket\Exception; -use Http\Client\Exception\NetworkException; - class ConnectionException extends NetworkException { } diff --git a/src/Exception/InvalidRequestException.php b/src/Exception/InvalidRequestException.php index fe77314..227375b 100644 --- a/src/Exception/InvalidRequestException.php +++ b/src/Exception/InvalidRequestException.php @@ -2,8 +2,6 @@ namespace Http\Client\Socket\Exception; -use Http\Client\Exception\NetworkException; - class InvalidRequestException extends NetworkException { } diff --git a/src/Exception/NetworkException.php b/src/Exception/NetworkException.php new file mode 100644 index 0000000..62d08a3 --- /dev/null +++ b/src/Exception/NetworkException.php @@ -0,0 +1,26 @@ +request = $request; + + parent::__construct($message, 0, $previous); + } + + public function getRequest(): RequestInterface + { + return $this->request; + } +} diff --git a/src/Exception/SSLConnectionException.php b/src/Exception/SSLConnectionException.php index 534a487..b1f85ef 100644 --- a/src/Exception/SSLConnectionException.php +++ b/src/Exception/SSLConnectionException.php @@ -2,8 +2,6 @@ namespace Http\Client\Socket\Exception; -use Http\Client\Exception\NetworkException; - class SSLConnectionException extends NetworkException { } diff --git a/src/Exception/StreamException.php b/src/Exception/StreamException.php index 6437cec..472472d 100644 --- a/src/Exception/StreamException.php +++ b/src/Exception/StreamException.php @@ -2,37 +2,8 @@ namespace Http\Client\Socket\Exception; -use Http\Client\Exception; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Client\ClientExceptionInterface; -class StreamException extends \RuntimeException implements Exception +class StreamException extends \RuntimeException implements ClientExceptionInterface { - /** - * The request object. - * - * @var RequestInterface - */ - private $request; - - /** - * Accepts an optional request object as 4th param. - * - * @param string $message - * @param int $code - * @param Exception $previous - * @param RequestInterface $request - */ - public function __construct($message = null, $code = null, $previous = null, RequestInterface $request = null) - { - $this->request = $request; - parent::__construct($message, $code, $previous); - } - - /** - * @return \Psr\Http\Message\RequestInterface|null - */ - final public function getRequest() - { - return $this->request; - } } diff --git a/src/Exception/TimeoutException.php b/src/Exception/TimeoutException.php index 9d05407..a821ab5 100644 --- a/src/Exception/TimeoutException.php +++ b/src/Exception/TimeoutException.php @@ -2,6 +2,6 @@ namespace Http\Client\Socket\Exception; -class TimeoutException extends StreamException +class TimeoutException extends NetworkException { } diff --git a/src/RequestWriter.php b/src/RequestWriter.php index 3e8ac88..0ac0430 100644 --- a/src/RequestWriter.php +++ b/src/RequestWriter.php @@ -17,13 +17,13 @@ trait RequestWriter /** * Write a request to a socket. * - * @param resource $socket - * @param RequestInterface $request - * @param int $bufferSize + * @param resource $socket + * + * @return void * * @throws BrokenPipeException */ - protected function writeRequest($socket, RequestInterface $request, $bufferSize = 8192) + protected function writeRequest($socket, RequestInterface $request, int $bufferSize = 8192) { if (false === $this->fwrite($socket, $this->transformRequestHeadersToString($request))) { throw new BrokenPipeException('Failed to send request, underlying socket not accessible, (BROKEN EPIPE)', $request); @@ -37,13 +37,13 @@ protected function writeRequest($socket, RequestInterface $request, $bufferSize /** * Write Body of the request. * - * @param resource $socket - * @param RequestInterface $request - * @param int $bufferSize + * @param resource $socket + * + * @return void * * @throws BrokenPipeException */ - protected function writeBody($socket, RequestInterface $request, $bufferSize = 8192) + protected function writeBody($socket, RequestInterface $request, int $bufferSize = 8192) { $body = $request->getBody(); @@ -62,12 +62,8 @@ protected function writeBody($socket, RequestInterface $request, $bufferSize = 8 /** * Produce the header of request as a string based on a PSR Request. - * - * @param RequestInterface $request - * - * @return string */ - protected function transformRequestHeadersToString(RequestInterface $request) + protected function transformRequestHeadersToString(RequestInterface $request): string { $message = vsprintf('%s %s HTTP/%s', [ strtoupper($request->getMethod()), @@ -90,11 +86,10 @@ protected function transformRequestHeadersToString(RequestInterface $request) * @see https://secure.phabricator.com/rPHU69490c53c9c2ef2002bc2dd4cecfe9a4b080b497 * * @param resource $stream The stream resource - * @param string $bytes Bytes written in the stream * * @return bool|int false if pipe is broken, number of bytes written otherwise */ - private function fwrite($stream, $bytes) + private function fwrite($stream, string $bytes) { if (!strlen($bytes)) { return 0; @@ -128,6 +123,7 @@ private function fwrite($stream, $bytes) // The write worked or failed explicitly. This value is fine to return. return $result; } + // We performed a 0-length write, were told that the stream was writable, and // then immediately performed another 0-length write. Conclude that the pipe // is broken and return `false`. diff --git a/src/ResponseReader.php b/src/ResponseReader.php index f5fd083..d5beae6 100644 --- a/src/ResponseReader.php +++ b/src/ResponseReader.php @@ -4,7 +4,7 @@ use Http\Client\Socket\Exception\BrokenPipeException; use Http\Client\Socket\Exception\TimeoutException; -use Http\Message\ResponseFactory; +use Nyholm\Psr7\Response; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -17,23 +17,15 @@ */ trait ResponseReader { - /** - * @var ResponseFactory For creating response - */ - protected $responseFactory; - /** * Read a response from a socket. * - * @param RequestInterface $request - * @param resource $socket + * @param resource $socket * * @throws TimeoutException When the socket timed out * @throws BrokenPipeException When the response cannot be read - * - * @return ResponseInterface */ - protected function readResponse(RequestInterface $request, $socket) + protected function readResponse(RequestInterface $request, $socket): ResponseInterface { $headers = []; $reason = null; @@ -48,10 +40,10 @@ protected function readResponse(RequestInterface $request, $socket) $metadatas = stream_get_meta_data($socket); if (array_key_exists('timed_out', $metadatas) && true === $metadatas['timed_out']) { - throw new TimeoutException('Error while reading response, stream timed out', null, null, $request); + throw new TimeoutException('Error while reading response, stream timed out', $request, null); } - - $parts = explode(' ', array_shift($headers), 3); + $header = array_shift($headers); + $parts = null !== $header ? explode(' ', $header, 3) : []; if (count($parts) <= 1) { throw new BrokenPipeException('Cannot read the response', $request); @@ -79,8 +71,8 @@ protected function readResponse(RequestInterface $request, $socket) : ''; } - $response = $this->responseFactory->createResponse($status, $reason, $responseHeaders, null, $protocol); - $stream = $this->createStream($socket, $response); + $response = new Response((int) $status, $responseHeaders, null, $protocol, $reason); + $stream = $this->createStream($socket, $request, $response); return $response->withBody($stream); } @@ -88,19 +80,19 @@ protected function readResponse(RequestInterface $request, $socket) /** * Create the stream. * - * @param $socket - * @param ResponseInterface $response - * - * @return Stream + * @param resource $socket */ - protected function createStream($socket, ResponseInterface $response) + protected function createStream($socket, RequestInterface $request, ResponseInterface $response): Stream { $size = null; if ($response->hasHeader('Content-Length')) { $size = (int) $response->getHeaderLine('Content-Length'); } + if ($size < 0) { + $size = null; + } - return new Stream($socket, $size); + return new Stream($request, $socket, $size); } } diff --git a/src/Stream.php b/src/Stream.php index 507f135..e49844a 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -4,6 +4,7 @@ use Http\Client\Socket\Exception\StreamException; use Http\Client\Socket\Exception\TimeoutException; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\StreamInterface; /** @@ -20,11 +21,13 @@ * * Writing and seeking is disable to avoid weird behaviors. * - * @author Joel Wurtz + * @author Joel Wurtz */ class Stream implements StreamInterface { - /** @var resource Underlying socket */ + /** @var resource|null Underlying socket */ private $socket; /** @@ -33,31 +36,34 @@ class Stream implements StreamInterface private $isDetached = false; /** - * @var int|null Size of the stream, so we know what we must read, null if not available (i.e. a chunked stream) + * @var int<0, max>|null Size of the stream, so we know what we must read, null if not available (i.e. a chunked stream) */ private $size; /** - * @var int Size of the stream readed, to avoid reading more than available and have the user blocked + * @var int<0, max> Size of the stream readed, to avoid reading more than available and have the user blocked */ private $readed = 0; + /** + * @var RequestInterface request associated to this stream + */ + private $request; + /** * Create the stream. * - * @param resource $socket - * @param int $size + * @param resource $socket + * @param int<0, max>|null $size */ - public function __construct($socket, $size = null) + public function __construct(RequestInterface $request, $socket, ?int $size = null) { $this->socket = $socket; $this->size = $size; + $this->request = $request; } - /** - * {@inheritdoc} - */ - public function __toString() + public function __toString(): string { try { return $this->getContents(); @@ -66,19 +72,19 @@ public function __toString() } } - /** - * {@inheritdoc} - */ - public function close() + public function close(): void { + if ($this->isDetached || null === $this->socket) { + throw new StreamException('Stream is detached'); + } fclose($this->socket); } - /** - * {@inheritdoc} - */ public function detach() { + if ($this->isDetached) { + return null; + } $this->isDetached = true; $socket = $this->socket; $this->socket = null; @@ -87,84 +93,85 @@ public function detach() } /** - * {@inheritdoc} + * @return int<0, max>|null */ - public function getSize() + public function getSize(): ?int { return $this->size; } - /** - * {@inheritdoc} - */ - public function tell() + public function tell(): int { - return ftell($this->socket); + if ($this->isDetached || null === $this->socket) { + throw new StreamException('Stream is detached'); + } + $tell = ftell($this->socket); + if (false === $tell) { + throw new StreamException('ftell returned false'); + } + + return $tell; } - /** - * {@inheritdoc} - */ - public function eof() + public function eof(): bool { + if ($this->isDetached || null === $this->socket) { + throw new StreamException('Stream is detached'); + } + return feof($this->socket); } - /** - * {@inheritdoc} - */ - public function isSeekable() + public function isSeekable(): bool { return false; } - /** - * {@inheritdoc} - */ - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { throw new StreamException('This stream is not seekable'); } - /** - * {@inheritdoc} - */ - public function rewind() + public function rewind(): void { throw new StreamException('This stream is not seekable'); } - /** - * {@inheritdoc} - */ - public function isWritable() + public function isWritable(): bool { return false; } - /** - * {@inheritdoc} - */ - public function write($string) + public function write($string): int { throw new StreamException('This stream is not writable'); } - /** - * {@inheritdoc} - */ - public function isReadable() + public function isReadable(): bool { return true; } /** - * {@inheritdoc} + * @param int<0, max> $length */ - public function read($length) + public function read($length): string { + if (0 === $length) { + return ''; + } + + if ($this->isDetached || null === $this->socket) { + throw new StreamException('Stream is detached'); + } + if (null === $this->getSize()) { - return fread($this->socket, $length); + $read = fread($this->socket, $length); + if (false === $read) { + throw new StreamException('Failed to read from stream'); + } + + return $read; } if ($this->getSize() === $this->readed) { @@ -173,9 +180,17 @@ public function read($length) // Even if we request a length a non blocking stream can return less data than asked $read = fread($this->socket, $length); + if (false === $read) { + // PHP 8 + if ($this->getMetadata('timed_out')) { + throw new TimeoutException('Stream timed out while reading data', $this->request); + } + throw new StreamException('Failed to read from stream'); + } + // PHP 7: fread does not return false when timing out if ($this->getMetadata('timed_out')) { - throw new TimeoutException('Stream timed out while reading data'); + throw new TimeoutException('Stream timed out while reading data', $this->request); } $this->readed += strlen($read); @@ -183,29 +198,38 @@ public function read($length) return $read; } - /** - * {@inheritdoc} - */ - public function getContents() + public function getContents(): string { + if ($this->isDetached || null === $this->socket) { + throw new StreamException('Stream is detached'); + } + if (null === $this->getSize()) { - return stream_get_contents($this->socket); + $contents = stream_get_contents($this->socket); + if (false === $contents) { + throw new StreamException('failed to get contents of stream'); + } + + return $contents; } $contents = ''; - do { - $contents .= $this->read($this->getSize() - $this->readed); - } while ($this->readed < $this->getSize()); + $toread = $this->getSize() - $this->readed; + while ($toread > 0) { + $contents .= $this->read($toread); + $toread = $this->getSize() - $this->readed; + } return $contents; } - /** - * {@inheritdoc} - */ public function getMetadata($key = null) { + if ($this->isDetached || null === $this->socket) { + throw new StreamException('Stream is detached'); + } + $meta = stream_get_meta_data($this->socket); if (null === $key) { diff --git a/ssl-macOS.md b/ssl-macOS.md new file mode 100644 index 0000000..c37468d --- /dev/null +++ b/ssl-macOS.md @@ -0,0 +1,58 @@ +# Generating SSL Certificates on macOS + +When generating SSL Certificates on macOS, you must ensure that you're using brew's openssl binary and not the one provided by the OS. + +To do that, find out where your openssl is installed by running: + +```bash +$ brew info openssl +``` + +You should see something like this: + +``` +openssl@1.1: stable 1.1.1i (bottled) [keg-only] +Cryptography and SSL/TLS Toolkit +https://openssl.org/ +/usr/local/Cellar/openssl@1.1/1.1.1i (8,067 files, 18.5MB) + Poured from bottle on 2020-12-11 at 11:31:46 +From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/openssl@1.1.rb +License: OpenSSL +==> Caveats +A CA file has been bootstrapped using certificates from the system +keychain. To add additional certificates, place .pem files in + /usr/local/etc/openssl@1.1/certs + +and run + /usr/local/opt/openssl@1.1/bin/c_rehash + +openssl@1.1 is keg-only, which means it was not symlinked into /usr/local, +because macOS provides LibreSSL. + +If you need to have openssl@1.1 first in your PATH run: + echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"' >> /Users/flavio/.bash_profile + +For compilers to find openssl@1.1 you may need to set: + export LDFLAGS="-L/usr/local/opt/openssl@1.1/lib" + export CPPFLAGS="-I/usr/local/opt/openssl@1.1/include" + +For pkg-config to find openssl@1.1 you may need to set: + export PKG_CONFIG_PATH="/usr/local/opt/openssl@1.1/lib/pkgconfig" + +==> Analytics +install: 855,315 (30 days), 2,356,331 (90 days), 7,826,269 (365 days) +install-on-request: 139,236 (30 days), 373,801 (90 days), 1,120,685 (365 days) +build-error: 0 (30 days) +``` + +The important part is this: + +> echo 'export PATH="/usr/local/opt/openssl@1.1/bin:$PATH"' >> /Users/flavio/.bash_profile + +Instead of running `./tests/server/ssl/generate.sh`, you should instead run: + +```bash +$ PATH="/usr/local/opt/openssl@1.1/bin ./tests/server/ssl/generate.sh +``` + +You should now be good to go. diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 3bc67d2..43d26a6 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -10,14 +10,14 @@ class BaseTestCase extends TestCase public function startServer($name) { - $filename = __DIR__ . '/server/' . $name . '.php'; - $pipes = []; + $filename = __DIR__.'/server/'.$name.'.php'; + $pipes = []; if (!Semaphore::acquire()) { $this->fail('Could not connect to server'); } - $this->servers[$name] = proc_open('php '. $filename, [], $pipes); + $this->servers[$name] = proc_open('php '.$filename, [], $pipes); sleep(1); } @@ -28,7 +28,7 @@ public function stopServer($name) } } - public function tearDown() + public function tearDown(): void { foreach (array_keys($this->servers) as $name) { $this->stopServer($name); diff --git a/tests/Semaphore.php b/tests/Semaphore.php index ea83010..9ff92a7 100644 --- a/tests/Semaphore.php +++ b/tests/Semaphore.php @@ -23,7 +23,7 @@ public static function acquire() return false; } } - self::$openConnections++; + ++self::$openConnections; return true; } @@ -35,6 +35,6 @@ public static function release() { // Do no be too quick usleep(500000); - self::$openConnections--; + --self::$openConnections; } } diff --git a/tests/SocketClientFeatureTest.php b/tests/SocketClientFeatureTest.php index 487944b..f1fe854 100644 --- a/tests/SocketClientFeatureTest.php +++ b/tests/SocketClientFeatureTest.php @@ -2,14 +2,39 @@ namespace Http\Client\Socket\Tests; -use Http\Client\Tests\HttpFeatureTest; -use Http\Message\MessageFactory\GuzzleMessageFactory; use Http\Client\Socket\Client as SocketHttpClient; +use Http\Client\Tests\HttpFeatureTest; +use Psr\Http\Client\ClientInterface; class SocketClientFeatureTest extends HttpFeatureTest { - protected function createClient() + protected function createClient(): ClientInterface { return new SocketHttpClient(); } + + public function testAutoSetContentLength(): void + { + $this->markTestSkipped('Feature is unsupported'); + } + + public function testGzip(): void + { + $this->markTestSkipped('Feature is unsupported'); + } + + public function testDeflate(): void + { + $this->markTestSkipped('Feature is unsupported'); + } + + public function testChunked(): void + { + $this->markTestSkipped('Feature is unsupported'); + } + + public function testRedirect(): void + { + $this->markTestSkipped('Feature is unsupported'); + } } diff --git a/tests/SocketHttpAdapterTest.php b/tests/SocketHttpAdapterTest.php index 3d90e70..72ed4bb 100644 --- a/tests/SocketHttpAdapterTest.php +++ b/tests/SocketHttpAdapterTest.php @@ -2,16 +2,13 @@ namespace Http\Client\Socket\Tests; -use Http\Client\Tests\HttpClientTest; -use Http\Message\MessageFactory\GuzzleMessageFactory; use Http\Client\Socket\Client as SocketHttpClient; +use Http\Client\Tests\HttpClientTest; +use Psr\Http\Client\ClientInterface; class SocketHttpAdapterTest extends HttpClientTest { - /** - * {@inheritdoc} - */ - protected function createHttpAdapter() + protected function createHttpAdapter(): ClientInterface { return new SocketHttpClient(); } diff --git a/tests/SocketHttpClientTest.php b/tests/SocketHttpClientTest.php index c3660ec..657f2ff 100644 --- a/tests/SocketHttpClientTest.php +++ b/tests/SocketHttpClientTest.php @@ -3,103 +3,96 @@ namespace Http\Client\Socket\Tests; use Http\Client\Common\HttpMethodsClient; -use Http\Message\MessageFactory\GuzzleMessageFactory; use Http\Client\Socket\Client as SocketHttpClient; +use Http\Client\Socket\Exception\NetworkException; +use Http\Client\Socket\Exception\TimeoutException; +use Nyholm\Psr7\Factory\Psr17Factory; class SocketHttpClientTest extends BaseTestCase { - public function createClient($options = array()) + public function createClient($options = []) { - $messageFactory = new GuzzleMessageFactory(); - - return new HttpMethodsClient(new SocketHttpClient($messageFactory, $options), $messageFactory); + return new HttpMethodsClient(new SocketHttpClient($options), new Psr17Factory()); } public function testTcpSocketDomain() { $this->startServer('tcp-server'); - $client = $this->createClient(['remote_socket' => '127.0.0.1:19999']); + $client = $this->createClient(['remote_socket' => '127.0.0.1:19999']); $response = $client->get('/', []); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - /** - * @expectedException \Http\Client\Exception\NetworkException - */ - public function testNoRemote() + public function testNoRemote(): void { - $client = $this->createClient(); + $client = $this->createClient(); + $this->expectException(NetworkException::class); $client->get('/', []); } - public function testRemoteInUri() + public function testRemoteInUri(): void { $this->startServer('tcp-server'); - $client = $this->createClient(); + $client = $this->createClient(); $response = $client->get('http://127.0.0.1:19999/', []); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - public function testRemoteInHostHeader() + public function testRemoteInHostHeader(): void { $this->startServer('tcp-server'); - $client = $this->createClient(); + $client = $this->createClient(); $response = $client->get('/', ['Host' => '127.0.0.1:19999']); $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - /** - * @expectedException \Http\Client\Exception\NetworkException - */ - public function testBrokenSocket() + public function testBrokenSocket(): void { $this->startServer('tcp-bugous-server'); $client = $this->createClient(['remote_socket' => '127.0.0.1:19999']); + $this->expectException(NetworkException::class); $client->get('/', []); } - public function testSslRemoteInUri() + public function testSslRemoteInUri(): void { $this->startServer('tcp-ssl-server'); - $client = $this->createClient([ + $client = $this->createClient([ + 'remote_socket' => 'tcp://127.0.0.1:19999', + 'ssl' => true, 'stream_context_options' => [ 'ssl' => [ 'peer_name' => 'socket-adapter', - 'cafile' => __DIR__ . '/server/ssl/ca.pem' - ] - ] + 'cafile' => __DIR__.'/server/ssl/ca.pem', + ], + ], ]); - $response = $client->get('https://127.0.0.1:19999/', []); + $response = $client->get('/', []); $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - public function testUnixSocketDomain() + public function testUnixSocketDomain(): void { $this->startServer('unix-domain-server'); - $client = $this->createClient([ - 'remote_socket' => 'unix://'.__DIR__.'/server/server.sock' + $client = $this->createClient([ + 'remote_socket' => 'unix://'.__DIR__.'/server/server.sock', ]); $response = $client->get('/', []); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - /** - * @expectedException \Http\Client\Exception\NetworkException - */ - public function testNetworkExceptionOnConnectError() + public function testNetworkExceptionOnConnectError(): void { - $client = $this->createClient(['remote_socket' => '127.0.0.1:19999']); + $client = $this->createClient(['remote_socket' => '127.0.0.1:19999']); + $this->expectException(NetworkException::class); $client->get('/', []); } @@ -107,88 +100,74 @@ public function testSslConnection() { $this->startServer('tcp-ssl-server'); - $client = $this->createClient([ + $client = $this->createClient([ 'remote_socket' => '127.0.0.1:19999', - 'ssl' => true, + 'ssl' => true, 'stream_context_options' => [ 'ssl' => [ 'peer_name' => 'socket-adapter', - 'cafile' => __DIR__ . '/server/ssl/ca.pem' - ] - ] + 'cafile' => __DIR__.'/server/ssl/ca.pem', + ], + ], ]); $response = $client->get('/', []); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - public function testSslConnectionWithClientCertificate() + public function testSslConnectionWithClientCertificate(): void { - if (version_compare(PHP_VERSION, '5.6', '<')) { - $this->markTestSkipped('Test can only run on php 5.6 and superior (for capturing peer certificate)'); - } - $this->startServer('tcp-ssl-server-client'); - $client = $this->createClient([ + $client = $this->createClient([ 'remote_socket' => '127.0.0.1:19999', - 'ssl' => true, + 'ssl' => true, 'stream_context_options' => [ 'ssl' => [ - 'peer_name' => 'socket-adapter', - 'cafile' => __DIR__ . '/server/ssl/ca.pem', - 'local_cert' => __DIR__ . '/server/ssl/client-and-key.pem' - ] - ] + 'peer_name' => 'socket-adapter', + 'cafile' => __DIR__.'/server/ssl/ca.pem', + 'local_cert' => __DIR__.'/server/ssl/client-and-key.pem', + ], + ], ]); $response = $client->get('/', []); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(200, $response->getStatusCode()); } - public function testInvalidSslConnectionWithClientCertificate() + public function testInvalidSslConnectionWithClientCertificate(): void { - if (version_compare(PHP_VERSION, '5.6', '<')) { - $this->markTestSkipped('Test can only run on php 5.6 and superior (for capturing peer certificate)'); - } - $this->startServer('tcp-ssl-server-client'); - $client = $this->createClient([ + $client = $this->createClient([ 'remote_socket' => '127.0.0.1:19999', - 'ssl' => true, + 'ssl' => true, 'stream_context_options' => [ 'ssl' => [ - 'peer_name' => 'socket-adapter', - 'cafile' => __DIR__ . '/server/ssl/ca.pem' - ] - ] + 'peer_name' => 'socket-adapter', + 'cafile' => __DIR__.'/server/ssl/ca.pem', + ], + ], ]); $response = $client->get('/', []); - $this->assertInstanceOf('Psr\Http\Message\ResponseInterface', $response); $this->assertEquals(403, $response->getStatusCode()); } - /** - * @expectedException \Http\Client\Exception\NetworkException - */ - public function testNetworkExceptionOnSslError() + public function testNetworkExceptionOnSslError(): void { $this->startServer('tcp-server'); - $client = $this->createClient(['remote_socket' => '127.0.0.1:19999', 'ssl' => true]); + $client = $this->createClient(['remote_socket' => '127.0.0.1:19999', 'ssl' => true]); + $this->expectException(NetworkException::class); $client->get('/', []); } - /** - * @expectedException \Http\Client\Exception\NetworkException - */ - public function testNetworkExceptionOnTimeout() + public function testNetworkExceptionOnTimeout(): void { - $client = $this->createClient(['timeout' => 10]); - $client->get('http://php.net', []); + $client = $this->createClient(['timeout' => 1]); + $this->expectException(TimeoutException::class); + $response = $client->get('https://php.net', []); + $response->getBody()->getContents(); } } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 2421fb1..ff40991 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -2,155 +2,146 @@ namespace Http\Client\Socket\Tests; +use Http\Client\Socket\Exception\StreamException; use Http\Client\Socket\Exception\TimeoutException; use Http\Client\Socket\Stream; +use Nyholm\Psr7\Request; use PHPUnit\Framework\TestCase; class StreamTest extends TestCase { - public function createSocket($body, $useSize = true) + public function createSocket($body, $useSize = true): Stream { - $socket = fopen('php://memory', 'rw'); + $socket = fopen('php://memory', 'rwb'); fwrite($socket, $body); fseek($socket, 0); - return new Stream($socket, $useSize ? strlen($body) : null); + return new Stream(new Request('GET', '/'), $socket, $useSize ? strlen($body) : null); } - public function testToString() + public function testToString(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); - $this->assertEquals("Body", $stream->__toString()); + $this->assertEquals('Body', $stream->__toString()); $stream->close(); } - public function testSubsequentCallIsEmpty() + public function testSubsequentCallIsEmpty(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); - $this->assertEquals("Body", $stream->getContents()); + $this->assertEquals('Body', $stream->getContents()); $this->assertEmpty($stream->getContents()); $stream->close(); } - public function testDetach() + public function testDetach(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); $socket = $stream->detach(); - $this->assertTrue(is_resource($socket)); + $this->assertIsResource($socket); $this->assertNull($stream->detach()); } - public function testTell() + public function testTell(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); $this->assertEquals(0, $stream->tell()); - $this->assertEquals("Body", $stream->getContents()); + $this->assertEquals('Body', $stream->getContents()); $this->assertEquals(4, $stream->tell()); } - public function testEof() + public function testEof(): void { - $socket = fopen('php://memory', 'rw+'); - fwrite($socket, "Body"); + $socket = fopen('php://memory', 'rwb+'); + fwrite($socket, 'Body'); fseek($socket, 0); - $stream = new Stream($socket); + $stream = new Stream(new Request('GET', '/'), $socket); - $this->assertEquals("Body", $stream->getContents()); + $this->assertEquals('Body', $stream->getContents()); fwrite($socket, "\0"); $this->assertTrue($stream->eof()); $stream->close(); } - public function testNotSeekable() + public function testNotSeekable(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); $this->assertFalse($stream->isSeekable()); - try { - $stream->seek(0); - } catch (\Exception $e) { - $this->assertInstanceOf('Http\Client\Socket\Exception\StreamException', $e); - } + $this->expectException(StreamException::class); + $stream->seek(0); } - public function testNoRewing() + public function testNoRewind(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); - try { - $stream->rewind(); - } catch (\Exception $e) { - $this->assertInstanceOf('Http\Client\Socket\Exception\StreamException', $e); - } + $this->expectException(StreamException::class); + $stream->rewind(); } - public function testNotWritable() + public function testNotWritable(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); $this->assertFalse($stream->isWritable()); - try { - $stream->write("Test"); - } catch (\Exception $e) { - $this->assertInstanceOf('Http\Client\Socket\Exception\StreamException', $e); - } + $this->expectException(StreamException::class); + $stream->write('Test'); } - public function testIsReadable() + public function testIsReadable(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); $this->assertTrue($stream->isReadable()); } - /** - * @expectedException \Http\Client\Socket\Exception\TimeoutException - */ - public function testTimeout() + public function testTimeout(): void { - $socket = fsockopen("php.net", 80); - socket_set_timeout($socket, 0, 100); + $socket = fsockopen('php.net', 80); + stream_set_timeout($socket, 0, 100); - $stream = new Stream($socket, 50); + $stream = new Stream(new Request('GET', '/'), $socket, 50); + $this->expectException(TimeoutException::class); $stream->getContents(); } - public function testMetadatas() + public function testMetadatas(): void { - $stream = $this->createSocket("Body", false); - - $this->assertEquals("PHP", $stream->getMetadata("wrapper_type")); - $this->assertEquals("MEMORY", $stream->getMetadata("stream_type")); - $this->assertEquals("php://memory", $stream->getMetadata("uri")); - $this->assertFalse($stream->getMetadata("timed_out")); - $this->assertFalse($stream->getMetadata("eof")); - $this->assertTrue($stream->getMetadata("blocked")); + $stream = $this->createSocket('Body', false); + + $this->assertEquals('PHP', $stream->getMetadata('wrapper_type')); + $this->assertEquals('MEMORY', $stream->getMetadata('stream_type')); + $this->assertEquals('php://memory', $stream->getMetadata('uri')); + $this->assertFalse($stream->getMetadata('timed_out')); + $this->assertFalse($stream->getMetadata('eof')); + $this->assertTrue($stream->getMetadata('blocked')); } - public function testClose() + public function testClose(): void { - $socket = fopen('php://memory', 'rw+'); - fwrite($socket, "Body"); + $socket = fopen('php://memory', 'rwb+'); + fwrite($socket, 'Body'); fseek($socket, 0); - $stream = new Stream($socket); + $stream = new Stream(new Request('GET', '/'), $socket); $stream->close(); - $this->assertFalse(is_resource($socket)); + $this->assertFalse(is_resource($socket)); // phpstorm thinks we could assertNotIsResource, but closed resources seem to behave differently } - public function testRead() + public function testRead(): void { - $stream = $this->createSocket("Body"); + $stream = $this->createSocket('Body'); - $this->assertEquals("Bod", $stream->read(3)); - $this->assertEquals("y", $stream->read(3)); + $this->assertEquals('Bod', $stream->read(3)); + $this->assertEquals('y', $stream->read(3)); $stream->close(); } diff --git a/tests/server/ssl/file.srl b/tests/server/ssl/file.srl index da51c42..a787364 100644 --- a/tests/server/ssl/file.srl +++ b/tests/server/ssl/file.srl @@ -1 +1 @@ -2F +34 diff --git a/tests/server/ssl/generate.sh b/tests/server/ssl/generate.sh index 696b340..8f1be48 100755 --- a/tests/server/ssl/generate.sh +++ b/tests/server/ssl/generate.sh @@ -1,5 +1,9 @@ #!/bin/bash +set -eo pipefail + +cd $(dirname $0) + C=FR ST=Ile-de-France L=Paris @@ -7,11 +11,11 @@ O="PHP-HTTP" CN="socket-adapter" openssl req -out ca.pem -new -x509 -subj "/C=$C/ST=$ST/L=$L/O=$O/CN=socket-server" -passout pass:password -openssl genrsa -out server.key 1024 -subj "/C=$C/ST=$ST/L=$L/O=$O/CN=socket-adapter" +openssl genrsa -out server.key openssl req -key server.key -new -out server.req -subj "/C=$C/ST=$ST/L=$L/O=$O/CN=socket-adapter" -passout pass:password openssl x509 -req -in server.req -CA ca.pem -CAkey privkey.pem -CAserial file.srl -out server.pem -passin pass:password -openssl genrsa -out client.key 1024 -subj "/C=$C/ST=$ST/L=$L/O=$O/CN=socket-adapter-client" +openssl genrsa -out client.key openssl req -key client.key -new -out client.req -subj "/C=$C/ST=$ST/L=$L/O=$O/CN=socket-adapter-client" -passout pass:password openssl x509 -req -in client.req -CA ca.pem -CAkey privkey.pem -CAserial file.srl -out client.pem -passin pass:password diff --git a/tests/server/tcp-bugous-server.php b/tests/server/tcp-bugous-server.php index 7f0d91f..723a6eb 100644 --- a/tests/server/tcp-bugous-server.php +++ b/tests/server/tcp-bugous-server.php @@ -1,9 +1,9 @@ [ - 'local_cert' => __DIR__ . '/ssl/server-and-key.pem', - 'cafile' => __DIR__ . '/ssl/ca.pem', - 'capture_peer_cert' => true - ] + 'local_cert' => __DIR__.'/ssl/server-and-key.pem', + 'cafile' => __DIR__.'/ssl/ca.pem', + 'capture_peer_cert' => true, + ], ]); -$socketServer = stream_socket_server('tcp://127.0.0.1:19999', $errNo, $errStr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context); +$socketServer = stream_socket_server('tcp://127.0.0.1:19999', $errNo, $errStr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); stream_socket_enable_crypto($socketServer, false); -$client = stream_socket_accept($socketServer); +$client = stream_socket_accept($socketServer); stream_set_blocking($client, true); -stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER); +stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLSv1_2_SERVER); // 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']; - $name = openssl_x509_parse($client_cert)["subject"]["CN"]; + $name = openssl_x509_parse($client_cert)['subject']['CN']; } -if ($name == "socket-adapter-client") { +if ('socket-adapter-client' == $name) { fwrite($client, str_replace("\n", "\r\n", << [ - 'local_cert' => __DIR__ . '/ssl/server-and-key.pem' - ] + 'local_cert' => __DIR__.'/ssl/server-and-key.pem', + ], ]); -$socketServer = stream_socket_server('tcp://127.0.0.1:19999', $errNo, $errStr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN, $context); +$socketServer = stream_socket_server('tcp://127.0.0.1:19999', $errNo, $errStr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $context); stream_socket_enable_crypto($socketServer, false); -$client = stream_socket_accept($socketServer); +$client = stream_socket_accept($socketServer); stream_set_blocking($client, true); -if (@stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLS_SERVER)) { +if (@stream_socket_enable_crypto($client, true, STREAM_CRYPTO_METHOD_TLSv1_2_SERVER)) { fwrite($client, str_replace("\n", "\r\n", <<