diff options
Diffstat (limited to 'vendor/react/http')
32 files changed, 5580 insertions, 0 deletions
diff --git a/vendor/react/http/LICENSE b/vendor/react/http/LICENSE new file mode 100644 index 0000000..d6f8901 --- /dev/null +++ b/vendor/react/http/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/react/http/composer.json b/vendor/react/http/composer.json new file mode 100644 index 0000000..4c9a038 --- /dev/null +++ b/vendor/react/http/composer.json @@ -0,0 +1,53 @@ +{ + "name": "react/http", + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": ["HTTP client", "HTTP server", "HTTP", "HTTPS", "event-driven", "streaming", "client", "server", "PSR-7", "async", "ReactPHP"], + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^2.3 || ^1.2.1", + "react/promise-stream": "^1.1", + "react/socket": "^1.9", + "react/stream": "^1.2", + "ringcentral/psr7": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.5", + "clue/http-proxy-react": "^1.7", + "clue/reactphp-ssh-proxy": "^1.3", + "clue/socks-react": "^1.3", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "autoload": { + "psr-4": { "React\\Http\\": "src" } + }, + "autoload-dev": { + "psr-4": { "React\\Tests\\Http\\": "tests" } + } +} diff --git a/vendor/react/http/src/Browser.php b/vendor/react/http/src/Browser.php new file mode 100644 index 0000000..72847f6 --- /dev/null +++ b/vendor/react/http/src/Browser.php @@ -0,0 +1,790 @@ +<?php + +namespace React\Http; + +use Psr\Http\Message\ResponseInterface; +use RingCentral\Psr7\Request; +use RingCentral\Psr7\Uri; +use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; +use React\Http\Io\ReadableBodyStream; +use React\Http\Io\Sender; +use React\Http\Io\Transaction; +use React\Promise\PromiseInterface; +use React\Socket\ConnectorInterface; +use React\Stream\ReadableStreamInterface; +use InvalidArgumentException; + +/** + * @final This class is final and shouldn't be extended as it is likely to be marked final in a future release. + */ +class Browser +{ + private $transaction; + private $baseUrl; + private $protocolVersion = '1.1'; + + /** + * The `Browser` is responsible for sending HTTP requests to your HTTP server + * and keeps track of pending incoming HTTP responses. + * + * ```php + * $browser = new React\Http\Browser(); + * ``` + * + * This class takes two optional arguments for more advanced usage: + * + * ```php + * // constructor signature as of v1.5.0 + * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + * + * // legacy constructor signature before v1.5.0 + * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); + * ``` + * + * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, + * proxy servers etc.), you can explicitly pass a custom instance of the + * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + * + * ```php + * $connector = new React\Socket\Connector(array( + * 'dns' => '127.0.0.1', + * 'tcp' => array( + * 'bindto' => '192.168.10.1:0' + * ), + * 'tls' => array( + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ) + * )); + * + * $browser = new React\Http\Browser($connector); + * ``` + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param null|ConnectorInterface|LoopInterface $connector + * @param null|LoopInterface|ConnectorInterface $loop + * @throws \InvalidArgumentException for invalid arguments + */ + public function __construct($connector = null, $loop = null) + { + // swap arguments for legacy constructor signature + if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { + $swap = $loop; + $loop = $connector; + $connector = $swap; + } + + if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { + throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); + } + + $loop = $loop ?: Loop::get(); + $this->transaction = new Transaction( + Sender::createFromLoop($loop, $connector), + $loop + ); + } + + /** + * Sends an HTTP GET request + * + * ```php + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [GET request client example](../examples/01-client-get-request.php). + * + * @param string $url URL for the request. + * @param array $headers + * @return PromiseInterface<ResponseInterface> + */ + public function get($url, array $headers = array()) + { + return $this->requestMayBeStreaming('GET', $url, $headers); + } + + /** + * Sends an HTTP POST request + * + * ```php + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [POST JSON client example](../examples/04-client-post-json.php). + * + * This method is also commonly used to submit HTML form data: + * + * ```php + * $data = [ + * 'user' => 'Alice', + * 'password' => 'secret' + * ]; + * + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/x-www-form-urlencoded' + * ], + * http_build_query($data) + * ); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->post($url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface<ResponseInterface> + */ + public function post($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('POST', $url, $headers, $body); + } + + /** + * Sends an HTTP HEAD request + * + * ```php + * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @return PromiseInterface<ResponseInterface> + */ + public function head($url, array $headers = array()) + { + return $this->requestMayBeStreaming('HEAD', $url, $headers); + } + + /** + * Sends an HTTP PATCH request + * + * ```php + * $browser->patch( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->patch($url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface<ResponseInterface> + */ + public function patch($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); + } + + /** + * Sends an HTTP PUT request + * + * ```php + * $browser->put( + * $url, + * [ + * 'Content-Type' => 'text/xml' + * ], + * $xml->asXML() + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [PUT XML client example](../examples/05-client-put-xml.php). + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->put($url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface<ResponseInterface> + */ + public function put($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('PUT', $url, $headers, $body); + } + + /** + * Sends an HTTP DELETE request + * + * ```php + * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface<ResponseInterface> + */ + public function delete($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface<ResponseInterface,\Exception> + */ + public function request($method, $url, array $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * In some situations, it's a better idea to use a streaming approach, where + * only small chunks have to be kept in memory. You can use this method to + * send an arbitrary HTTP request and receive a streaming response. It uses + * the same HTTP message API, but does not buffer the response body in + * memory. It only processes the response body in small chunks as data is + * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). + * This works for (any number of) responses of arbitrary sizes. + * + * ```php + * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * $body = $response->getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * $body->on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $body->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * + * $body->on('close', function () { + * echo '[DONE]' . PHP_EOL; + * }); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + * and the [streaming response](#streaming-response) for more details, + * examples and possible use-cases. + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface<ResponseInterface,\Exception> + */ + public function requestStreaming($method, $url, $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Changes the maximum timeout used for waiting for pending requests. + * + * You can pass in the number of seconds to use as a new timeout value: + * + * ```php + * $browser = $browser->withTimeout(10.0); + * ``` + * + * You can pass in a bool `false` to disable any timeouts. In this case, + * requests can stay pending forever: + * + * ```php + * $browser = $browser->withTimeout(false); + * ``` + * + * You can pass in a bool `true` to re-enable default timeout handling. This + * will respects PHP's `default_socket_timeout` setting (default 60s): + * + * ```php + * $browser = $browser->withTimeout(true); + * ``` + * + * See also [timeouts](#timeouts) for more details about timeout handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given timeout value applied. + * + * @param bool|number $timeout + * @return self + */ + public function withTimeout($timeout) + { + if ($timeout === true) { + $timeout = null; + } elseif ($timeout === false) { + $timeout = -1; + } elseif ($timeout < 0) { + $timeout = 0; + } + + return $this->withOptions(array( + 'timeout' => $timeout, + )); + } + + /** + * Changes how HTTP redirects will be followed. + * + * You can pass in the maximum number of redirects to follow: + * + * ```php + * $browser = $browser->withFollowRedirects(5); + * ``` + * + * The request will automatically be rejected when the number of redirects + * is exceeded. You can pass in a `0` to reject the request for any + * redirects encountered: + * + * ```php + * $browser = $browser->withFollowRedirects(0); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // only non-redirected responses will now end up here + * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * You can pass in a bool `false` to disable following any redirects. In + * this case, requests will resolve with the redirection response instead + * of following the `Location` response header: + * + * ```php + * $browser = $browser->withFollowRedirects(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any redirects will now end up here + * var_dump($response->getHeaderLine('Location')); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default redirect handling. + * This defaults to following a maximum of 10 redirects: + * + * ```php + * $browser = $browser->withFollowRedirects(true); + * ``` + * + * See also [redirects](#redirects) for more details about redirect handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given redirect setting applied. + * + * @param bool|int $followRedirects + * @return self + */ + public function withFollowRedirects($followRedirects) + { + return $this->withOptions(array( + 'followRedirects' => $followRedirects !== false, + 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects + )); + } + + /** + * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + * + * You can pass in a bool `false` to disable rejecting incoming responses + * that use a 4xx or 5xx response status code. In this case, requests will + * resolve with the response message indicating an error condition: + * + * ```php + * $browser = $browser->withRejectErrorResponse(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default status code handling. + * This defaults to rejecting any response status codes in the 4xx or 5xx + * range: + * + * ```php + * $browser = $browser->withRejectErrorResponse(true); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any successful HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * if ($e instanceof React\Http\Message\ResponseException) { + * // any HTTP response error message will now end up here + * $response = $e->getResponse(); + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * } else { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * } + * }); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param bool $obeySuccessCode + * @return self + */ + public function withRejectErrorResponse($obeySuccessCode) + { + return $this->withOptions(array( + 'obeySuccessCode' => $obeySuccessCode, + )); + } + + /** + * Changes the base URL used to resolve relative URLs to. + * + * If you configure a base URL, any requests to relative URLs will be + * processed by first resolving this relative to the given absolute base + * URL. This supports resolving relative path references (like `../` etc.). + * This is particularly useful for (RESTful) API calls where all endpoints + * (URLs) are located under a common base URL. + * + * ```php + * $browser = $browser->withBase('http://api.example.com/v3/'); + * + * // will request http://api.example.com/v3/users + * $browser->get('users')->then(…); + * ``` + * + * You can pass in a `null` base URL to return a new instance that does not + * use a base URL: + * + * ```php + * $browser = $browser->withBase(null); + * ``` + * + * Accordingly, any requests using relative URLs to a browser that does not + * use a base URL can not be completed and will be rejected without sending + * a request. + * + * This method will throw an `InvalidArgumentException` if the given + * `$baseUrl` argument is not a valid URL. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method + * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + * + * @param string|null $baseUrl absolute base URL + * @return self + * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL + * @see self::withoutBase() + */ + public function withBase($baseUrl) + { + $browser = clone $this; + if ($baseUrl === null) { + $browser->baseUrl = null; + return $browser; + } + + $browser->baseUrl = new Uri($baseUrl); + if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + throw new \InvalidArgumentException('Base URL must be absolute'); + } + + return $browser; + } + + /** + * Changes the HTTP protocol version that will be used for all subsequent requests. + * + * All the above [request methods](#request-methods) default to sending + * requests as HTTP/1.1. This is the preferred HTTP protocol version which + * also provides decent backwards-compatibility with legacy HTTP/1.0 + * servers. As such, there should rarely be a need to explicitly change this + * protocol version. + * + * If you want to explicitly use the legacy HTTP/1.0 protocol version, you + * can use this method: + * + * ```php + * $browser = $browser->withProtocolVersion('1.0'); + * + * $browser->get($url)->then(…); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * new protocol version applied. + * + * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" + * @return self + * @throws InvalidArgumentException + */ + public function withProtocolVersion($protocolVersion) + { + if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); + } + + $browser = clone $this; + $browser->protocolVersion = (string) $protocolVersion; + + return $browser; + } + + /** + * Changes the maximum size for buffering a response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * By default, the response body buffer will be limited to 16 MiB. If the + * response body exceeds this maximum size, the request will be rejected. + * + * You can pass in the maximum number of bytes to buffer: + * + * ```php + * $browser = $browser->withResponseBuffer(1024 * 1024); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // response body will not exceed 1 MiB + * var_dump($response->getHeaders(), (string) $response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * Note that the response body buffer has to be kept in memory for each + * pending request until its transfer is completed and it will only be freed + * after a pending request is fulfilled. As such, increasing this maximum + * buffer size to allow larger response bodies is usually not recommended. + * Instead, you can use the [`requestStreaming()` method](#requeststreaming) + * to receive responses with arbitrary sizes without buffering. Accordingly, + * this maximum buffer size setting has no effect on streaming responses. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param int $maximumSize + * @return self + * @see self::requestStreaming() + */ + public function withResponseBuffer($maximumSize) + { + return $this->withOptions(array( + 'maximumSize' => $maximumSize + )); + } + + /** + * Changes the [options](#options) to use: + * + * The [`Browser`](#browser) class exposes several options for the handling of + * HTTP transactions. These options resemble some of PHP's + * [HTTP context options](http://php.net/manual/en/context.http.php) and + * can be controlled via the following API (and their defaults): + * + * ```php + * // deprecated + * $newBrowser = $browser->withOptions(array( + * 'timeout' => null, // see withTimeout() instead + * 'followRedirects' => true, // see withFollowRedirects() instead + * 'maxRedirects' => 10, // see withFollowRedirects() instead + * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + * 'streaming' => false, // deprecated, see requestStreaming() instead + * )); + * ``` + * + * See also [timeouts](#timeouts), [redirects](#redirects) and + * [streaming](#streaming) for more details. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * options applied. + * + * @param array $options + * @return self + * @see self::withTimeout() + * @see self::withFollowRedirects() + * @see self::withRejectErrorResponse() + */ + private function withOptions(array $options) + { + $browser = clone $this; + $browser->transaction = $this->transaction->withOptions($options); + + return $browser; + } + + /** + * @param string $method + * @param string $url + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface<ResponseInterface,\Exception> + */ + private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') + { + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $url = Uri::resolve($this->baseUrl, $url); + } + + if ($body instanceof ReadableStreamInterface) { + $body = new ReadableBodyStream($body); + } + + return $this->transaction->send( + new Request($method, $url, $headers, $body, $this->protocolVersion) + ); + } +} diff --git a/vendor/react/http/src/Client/Client.php b/vendor/react/http/src/Client/Client.php new file mode 100644 index 0000000..7a97349 --- /dev/null +++ b/vendor/react/http/src/Client/Client.php @@ -0,0 +1,31 @@ +<?php + +namespace React\Http\Client; + +use React\EventLoop\LoopInterface; +use React\Socket\ConnectorInterface; +use React\Socket\Connector; + +/** + * @internal + */ +class Client +{ + private $connector; + + public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + { + if ($connector === null) { + $connector = new Connector(array(), $loop); + } + + $this->connector = $connector; + } + + public function request($method, $url, array $headers = array(), $protocolVersion = '1.0') + { + $requestData = new RequestData($method, $url, $headers, $protocolVersion); + + return new Request($this->connector, $requestData); + } +} diff --git a/vendor/react/http/src/Client/Request.php b/vendor/react/http/src/Client/Request.php new file mode 100644 index 0000000..51e0331 --- /dev/null +++ b/vendor/react/http/src/Client/Request.php @@ -0,0 +1,237 @@ +<?php + +namespace React\Http\Client; + +use Evenement\EventEmitter; +use React\Promise; +use React\Socket\ConnectionInterface; +use React\Socket\ConnectorInterface; +use React\Stream\WritableStreamInterface; +use RingCentral\Psr7 as gPsr; + +/** + * @event response + * @event drain + * @event error + * @event end + * @internal + */ +class Request extends EventEmitter implements WritableStreamInterface +{ + const STATE_INIT = 0; + const STATE_WRITING_HEAD = 1; + const STATE_HEAD_WRITTEN = 2; + const STATE_END = 3; + + private $connector; + private $requestData; + + private $stream; + private $buffer; + private $responseFactory; + private $state = self::STATE_INIT; + private $ended = false; + + private $pendingWrites = ''; + + public function __construct(ConnectorInterface $connector, RequestData $requestData) + { + $this->connector = $connector; + $this->requestData = $requestData; + } + + public function isWritable() + { + return self::STATE_END > $this->state && !$this->ended; + } + + private function writeHead() + { + $this->state = self::STATE_WRITING_HEAD; + + $requestData = $this->requestData; + $streamRef = &$this->stream; + $stateRef = &$this->state; + $pendingWrites = &$this->pendingWrites; + $that = $this; + + $promise = $this->connect(); + $promise->then( + function (ConnectionInterface $stream) use ($requestData, &$streamRef, &$stateRef, &$pendingWrites, $that) { + $streamRef = $stream; + + $stream->on('drain', array($that, 'handleDrain')); + $stream->on('data', array($that, 'handleData')); + $stream->on('end', array($that, 'handleEnd')); + $stream->on('error', array($that, 'handleError')); + $stream->on('close', array($that, 'handleClose')); + + $headers = (string) $requestData; + + $more = $stream->write($headers . $pendingWrites); + + $stateRef = Request::STATE_HEAD_WRITTEN; + + // clear pending writes if non-empty + if ($pendingWrites !== '') { + $pendingWrites = ''; + + if ($more) { + $that->emit('drain'); + } + } + }, + array($this, 'closeError') + ); + + $this->on('close', function() use ($promise) { + $promise->cancel(); + }); + } + + public function write($data) + { + if (!$this->isWritable()) { + return false; + } + + // write directly to connection stream if already available + if (self::STATE_HEAD_WRITTEN <= $this->state) { + return $this->stream->write($data); + } + + // otherwise buffer and try to establish connection + $this->pendingWrites .= $data; + if (self::STATE_WRITING_HEAD > $this->state) { + $this->writeHead(); + } + + return false; + } + + public function end($data = null) + { + if (!$this->isWritable()) { + return; + } + + if (null !== $data) { + $this->write($data); + } else if (self::STATE_WRITING_HEAD > $this->state) { + $this->writeHead(); + } + + $this->ended = true; + } + + /** @internal */ + public function handleDrain() + { + $this->emit('drain'); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + // buffer until double CRLF (or double LF for compatibility with legacy servers) + if (false !== strpos($this->buffer, "\r\n\r\n") || false !== strpos($this->buffer, "\n\n")) { + try { + $response = gPsr\parse_response($this->buffer); + $bodyChunk = (string) $response->getBody(); + } catch (\InvalidArgumentException $exception) { + $this->emit('error', array($exception)); + } + + $this->buffer = null; + + $this->stream->removeListener('drain', array($this, 'handleDrain')); + $this->stream->removeListener('data', array($this, 'handleData')); + $this->stream->removeListener('end', array($this, 'handleEnd')); + $this->stream->removeListener('error', array($this, 'handleError')); + $this->stream->removeListener('close', array($this, 'handleClose')); + + if (!isset($response)) { + return; + } + + $this->stream->on('close', array($this, 'handleClose')); + + $this->emit('response', array($response, $this->stream)); + + $this->stream->emit('data', array($bodyChunk)); + } + } + + /** @internal */ + public function handleEnd() + { + $this->closeError(new \RuntimeException( + "Connection ended before receiving response" + )); + } + + /** @internal */ + public function handleError(\Exception $error) + { + $this->closeError(new \RuntimeException( + "An error occurred in the underlying stream", + 0, + $error + )); + } + + /** @internal */ + public function handleClose() + { + $this->close(); + } + + /** @internal */ + public function closeError(\Exception $error) + { + if (self::STATE_END <= $this->state) { + return; + } + $this->emit('error', array($error)); + $this->close(); + } + + public function close() + { + if (self::STATE_END <= $this->state) { + return; + } + + $this->state = self::STATE_END; + $this->pendingWrites = ''; + + if ($this->stream) { + $this->stream->close(); + } + + $this->emit('close'); + $this->removeAllListeners(); + } + + protected function connect() + { + $scheme = $this->requestData->getScheme(); + if ($scheme !== 'https' && $scheme !== 'http') { + return Promise\reject( + new \InvalidArgumentException('Invalid request URL given') + ); + } + + $host = $this->requestData->getHost(); + $port = $this->requestData->getPort(); + + if ($scheme === 'https') { + $host = 'tls://' . $host; + } + + return $this->connector + ->connect($host . ':' . $port); + } +} diff --git a/vendor/react/http/src/Client/RequestData.php b/vendor/react/http/src/Client/RequestData.php new file mode 100644 index 0000000..a5908a0 --- /dev/null +++ b/vendor/react/http/src/Client/RequestData.php @@ -0,0 +1,128 @@ +<?php + +namespace React\Http\Client; + +/** + * @internal + */ +class RequestData +{ + private $method; + private $url; + private $headers; + private $protocolVersion; + + public function __construct($method, $url, array $headers = array(), $protocolVersion = '1.0') + { + $this->method = $method; + $this->url = $url; + $this->headers = $headers; + $this->protocolVersion = $protocolVersion; + } + + private function mergeDefaultheaders(array $headers) + { + $port = ($this->getDefaultPort() === $this->getPort()) ? '' : ":{$this->getPort()}"; + $connectionHeaders = ('1.1' === $this->protocolVersion) ? array('Connection' => 'close') : array(); + $authHeaders = $this->getAuthHeaders(); + + $defaults = array_merge( + array( + 'Host' => $this->getHost().$port, + 'User-Agent' => 'ReactPHP/1', + ), + $connectionHeaders, + $authHeaders + ); + + // remove all defaults that already exist in $headers + $lower = array_change_key_case($headers, CASE_LOWER); + foreach ($defaults as $key => $_) { + if (isset($lower[strtolower($key)])) { + unset($defaults[$key]); + } + } + + return array_merge($defaults, $headers); + } + + public function getScheme() + { + return parse_url($this->url, PHP_URL_SCHEME); + } + + public function getHost() + { + return parse_url($this->url, PHP_URL_HOST); + } + + public function getPort() + { + return (int) parse_url($this->url, PHP_URL_PORT) ?: $this->getDefaultPort(); + } + + public function getDefaultPort() + { + return ('https' === $this->getScheme()) ? 443 : 80; + } + + public function getPath() + { + $path = parse_url($this->url, PHP_URL_PATH); + $queryString = parse_url($this->url, PHP_URL_QUERY); + + // assume "/" path by default, but allow "OPTIONS *" + if ($path === null) { + $path = ($this->method === 'OPTIONS' && $queryString === null) ? '*': '/'; + } + if ($queryString !== null) { + $path .= '?' . $queryString; + } + + return $path; + } + + public function setProtocolVersion($version) + { + $this->protocolVersion = $version; + } + + public function __toString() + { + $headers = $this->mergeDefaultheaders($this->headers); + + $data = ''; + $data .= "{$this->method} {$this->getPath()} HTTP/{$this->protocolVersion}\r\n"; + foreach ($headers as $name => $values) { + foreach ((array)$values as $value) { + $data .= "$name: $value\r\n"; + } + } + $data .= "\r\n"; + + return $data; + } + + private function getUrlUserPass() + { + $components = parse_url($this->url); + + if (isset($components['user'])) { + return array( + 'user' => $components['user'], + 'pass' => isset($components['pass']) ? $components['pass'] : null, + ); + } + } + + private function getAuthHeaders() + { + if (null !== $auth = $this->getUrlUserPass()) { + return array( + 'Authorization' => 'Basic ' . base64_encode($auth['user'].':'.$auth['pass']), + ); + } + + return array(); + } +} diff --git a/vendor/react/http/src/HttpServer.php b/vendor/react/http/src/HttpServer.php new file mode 100644 index 0000000..f233473 --- /dev/null +++ b/vendor/react/http/src/HttpServer.php @@ -0,0 +1,351 @@ +<?php + +namespace React\Http; + +use Evenement\EventEmitter; +use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; +use React\Http\Io\IniUtil; +use React\Http\Io\MiddlewareRunner; +use React\Http\Io\StreamingServer; +use React\Http\Middleware\LimitConcurrentRequestsMiddleware; +use React\Http\Middleware\StreamingRequestMiddleware; +use React\Http\Middleware\RequestBodyBufferMiddleware; +use React\Http\Middleware\RequestBodyParserMiddleware; +use React\Socket\ServerInterface; + +/** + * The `React\Http\HttpServer` class is responsible for handling incoming connections and then + * processing each incoming HTTP request. + * + * When a complete HTTP request has been received, it will invoke the given + * request handler function. This request handler function needs to be passed to + * the constructor and will be invoked with the respective [request](#server-request) + * object and expects a [response](#server-response) object in return: + * + * ```php + * $http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + * return new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * array( + * 'Content-Type' => 'text/plain' + * ), + * "Hello World!\n" + * ); + * }); + * ``` + * + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#server-request) chapter for more details. + * + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#server-response) chapter for more details. + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * In order to start listening for any incoming connections, the `HttpServer` needs + * to be attached to an instance of + * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * through the [`listen()`](#listen) method as described in the following + * chapter. In its most simple form, you can attach this to a + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $http = new React\Http\HttpServer($handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); + * $http->listen($socket); + * ``` + * + * See also the [`listen()`](#listen) method and + * [hello world server example](../examples/51-server-hello-world.php) + * for more details. + * + * By default, the `HttpServer` buffers and parses the complete incoming HTTP + * request in memory. It will invoke the given request handler function when the + * complete request headers and request body has been received. This means the + * [request](#server-request) object passed to your request handler function will be + * fully compatible with PSR-7 (http-message). This provides sane defaults for + * 80% of the use cases and is the recommended way to use this library unless + * you're sure you know what you're doing. + * + * On the other hand, buffering complete HTTP requests in memory until they can + * be processed by your request handler function means that this class has to + * employ a number of limits to avoid consuming too much memory. In order to + * take the more advanced configuration out your hand, it respects setting from + * your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its + * default settings. This is a list of PHP settings this class respects with + * their respective default values: + * + * ``` + * memory_limit 128M + * post_max_size 8M // capped at 64K + * + * enable_post_data_reading 1 + * max_input_nesting_level 64 + * max_input_vars 1000 + * + * file_uploads 1 + * upload_max_filesize 2M + * max_file_uploads 20 + * ``` + * + * In particular, the `post_max_size` setting limits how much memory a single + * HTTP request is allowed to consume while buffering its request body. This + * needs to be limited because the server can process a large number of requests + * concurrently, so the server may potentially consume a large amount of memory + * otherwise. To support higher concurrency by default, this value is capped + * at `64K`. If you assign a higher value, it will only allow `64K` by default. + * If a request exceeds this limit, its request body will be ignored and it will + * be processed like a request with no request body at all. See below for + * explicit configuration to override this setting. + * + * By default, this class will try to avoid consuming more than half of your + * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with + * the above default settings of `128M` max, it will try to consume no more than + * `64M` for buffering multiple concurrent HTTP requests. As a consequence, it + * will limit the concurrency to `1024` HTTP requests with the above defaults. + * + * It is imperative that you assign reasonable values to your PHP ini settings. + * It is usually recommended to not support buffering incoming HTTP requests + * with a large HTTP request body (e.g. large file uploads). If you want to + * increase this buffer size, you will have to also increase the total memory + * limit to allow for more concurrent requests (set `memory_limit 512M` or more) + * or explicitly limit concurrency. + * + * In order to override the above buffering defaults, you can configure the + * `HttpServer` explicitly. You can use the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once like this: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * )); + * ``` + * + * In this example, we allow processing up to 100 concurrent requests at once + * and each request can buffer up to `2M`. This means you may have to keep a + * maximum of `200M` of memory for incoming request body buffers. Accordingly, + * you need to adjust the `memory_limit` ini setting to allow for these buffers + * plus your actual application logic memory requirements (think `512M` or more). + * + * > Internally, this class automatically assigns these middleware handlers + * automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + * is given. Accordingly, you can use this example to override all default + * settings to implement custom limits. + * + * As an alternative to buffering the complete request body in memory, you can + * also use a streaming approach where only small chunks of data have to be kept + * in memory: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * $handler + * ); + * ``` + * + * In this case, it will invoke the request handler function once the HTTP + * request headers have been received, i.e. before receiving the potentially + * much larger HTTP request body. This means the [request](#server-request) passed to + * your request handler function may not be fully compatible with PSR-7. This is + * specifically designed to help with more advanced use cases where you want to + * have full control over consuming the incoming HTTP request body and + * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) + * below for more details. + * + * > Changelog v1.5.0: This class has been renamed to `HttpServer` from the + * previous `Server` class in order to avoid any ambiguities. + * The previous name has been deprecated and should not be used anymore. + */ +final class HttpServer extends EventEmitter +{ + /** + * The maximum buffer size used for each request. + * + * This needs to be limited because the server can process a large number of + * requests concurrently, so the server may potentially consume a large + * amount of memory otherwise. + * + * See `RequestBodyBufferMiddleware` to override this setting. + * + * @internal + */ + const MAXIMUM_BUFFER_SIZE = 65536; // 64 KiB + + /** + * @var StreamingServer + */ + private $streamingServer; + + /** + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param callable|LoopInterface $requestHandlerOrLoop + * @param callable[] ...$requestHandler + * @see self::listen() + */ + public function __construct($requestHandlerOrLoop) + { + $requestHandlers = \func_get_args(); + if (reset($requestHandlers) instanceof LoopInterface) { + $loop = \array_shift($requestHandlers); + } else { + $loop = Loop::get(); + } + + $requestHandlersCount = \count($requestHandlers); + if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) { + throw new \InvalidArgumentException('Invalid request handler given'); + } + + $streaming = false; + foreach ((array) $requestHandlers as $handler) { + if ($handler instanceof StreamingRequestMiddleware) { + $streaming = true; + break; + } + } + + $middleware = array(); + if (!$streaming) { + $maxSize = $this->getMaxRequestSize(); + $concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize); + if ($concurrency !== null) { + $middleware[] = new LimitConcurrentRequestsMiddleware($concurrency); + } + $middleware[] = new RequestBodyBufferMiddleware($maxSize); + // Checking for an empty string because that is what a boolean + // false is returned as by ini_get depending on the PHP version. + // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading + // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes + // @link https://3v4l.org/qJtsa + $enablePostDataReading = \ini_get('enable_post_data_reading'); + if ($enablePostDataReading !== '') { + $middleware[] = new RequestBodyParserMiddleware(); + } + } + + $middleware = \array_merge($middleware, $requestHandlers); + + /** + * Filter out any configuration middleware, no need to run requests through something that isn't + * doing anything with the request. + */ + $middleware = \array_filter($middleware, function ($handler) { + return !($handler instanceof StreamingRequestMiddleware); + }); + + $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); + + $that = $this; + $this->streamingServer->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + } + + /** + * Starts listening for HTTP requests on the given socket server instance + * + * The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * is responsible for emitting the underlying streaming connections. This + * HTTP server needs to be attached to it in order to process any + * connections and pase incoming streaming data as incoming HTTP request + * messages. In its most common form, you can attach this to a + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $http = new React\Http\HttpServer($handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); + * $http->listen($socket); + * ``` + * + * See also [hello world server example](../examples/51-server-hello-world.php) + * for more details. + * + * This example will start listening for HTTP requests on the alternative + * HTTP port `8080` on all interfaces (publicly). As an alternative, it is + * very common to use a reverse proxy and let this HTTP server listen on the + * localhost (loopback) interface only by using the listen address + * `127.0.0.1:8080` instead. This way, you host your application(s) on the + * default HTTP port `80` and only route specific requests to this HTTP + * server. + * + * Likewise, it's usually recommended to use a reverse proxy setup to accept + * secure HTTPS requests on default HTTPS port `443` (TLS termination) and + * only route plaintext requests to this HTTP server. As an alternative, you + * can also accept secure HTTPS requests with this HTTP server by attaching + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * using a secure TLS listen address, a certificate file and optional + * `passphrase` like this: + * + * ```php + * $http = new React\Http\HttpServer($handler); + * + * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + * 'tls' => array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * ) + * )); + * $http->listen($socket); + * ``` + * + * See also [hello world HTTPS example](../examples/61-server-hello-world-https.php) + * for more details. + * + * @param ServerInterface $socket + */ + public function listen(ServerInterface $socket) + { + $this->streamingServer->listen($socket); + } + + /** + * @param string $memory_limit + * @param string $post_max_size + * @return ?int + */ + private function getConcurrentRequestsLimit($memory_limit, $post_max_size) + { + if ($memory_limit == -1) { + return null; + } + + $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 2; + $concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size)); + + return $concurrentRequests; + } + + /** + * @param ?string $post_max_size + * @return int + */ + private function getMaxRequestSize($post_max_size = null) + { + $maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size); + + return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize; + } +} diff --git a/vendor/react/http/src/Io/BufferedBody.php b/vendor/react/http/src/Io/BufferedBody.php new file mode 100644 index 0000000..4a4d839 --- /dev/null +++ b/vendor/react/http/src/Io/BufferedBody.php @@ -0,0 +1,179 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\StreamInterface; + +/** + * [Internal] PSR-7 message body implementation using an in-memory buffer + * + * @internal + */ +class BufferedBody implements StreamInterface +{ + private $buffer = ''; + private $position = 0; + private $closed = false; + + /** + * @param string $buffer + */ + public function __construct($buffer) + { + $this->buffer = $buffer; + } + + public function __toString() + { + if ($this->closed) { + return ''; + } + + $this->seek(0); + + return $this->getContents(); + } + + public function close() + { + $this->buffer = ''; + $this->position = 0; + $this->closed = true; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize() + { + return $this->closed ? null : \strlen($this->buffer); + } + + public function tell() + { + if ($this->closed) { + throw new \RuntimeException('Unable to tell position of closed stream'); + } + + return $this->position; + } + + public function eof() + { + return $this->position >= \strlen($this->buffer); + } + + public function isSeekable() + { + return !$this->closed; + } + + public function seek($offset, $whence = \SEEK_SET) + { + if ($this->closed) { + throw new \RuntimeException('Unable to seek on closed stream'); + } + + $old = $this->position; + + if ($whence === \SEEK_SET) { + $this->position = $offset; + } elseif ($whence === \SEEK_CUR) { + $this->position += $offset; + } elseif ($whence === \SEEK_END) { + $this->position = \strlen($this->buffer) + $offset; + } else { + throw new \InvalidArgumentException('Invalid seek mode given'); + } + + if (!\is_int($this->position) || $this->position < 0) { + $this->position = $old; + throw new \RuntimeException('Unable to seek to position'); + } + } + + public function rewind() + { + $this->seek(0); + } + + public function isWritable() + { + return !$this->closed; + } + + public function write($string) + { + if ($this->closed) { + throw new \RuntimeException('Unable to write to closed stream'); + } + + if ($string === '') { + return 0; + } + + if ($this->position > 0 && !isset($this->buffer[$this->position - 1])) { + $this->buffer = \str_pad($this->buffer, $this->position, "\0"); + } + + $len = \strlen($string); + $this->buffer = \substr($this->buffer, 0, $this->position) . $string . \substr($this->buffer, $this->position + $len); + $this->position += $len; + + return $len; + } + + public function isReadable() + { + return !$this->closed; + } + + public function read($length) + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if ($length < 1) { + throw new \InvalidArgumentException('Invalid read length given'); + } + + if ($this->position + $length > \strlen($this->buffer)) { + $length = \strlen($this->buffer) - $this->position; + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position += $length; + + return \substr($this->buffer, $pos, $length); + } + + public function getContents() + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position = \strlen($this->buffer); + + return \substr($this->buffer, $pos); + } + + public function getMetadata($key = null) + { + return $key === null ? array() : null; + } +} diff --git a/vendor/react/http/src/Io/ChunkedDecoder.php b/vendor/react/http/src/Io/ChunkedDecoder.php new file mode 100644 index 0000000..2f58f42 --- /dev/null +++ b/vendor/react/http/src/Io/ChunkedDecoder.php @@ -0,0 +1,175 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; +use Exception; + +/** + * [Internal] Decodes "Transfer-Encoding: chunked" from given stream and returns only payload data. + * + * This is used internally to decode incoming requests with this encoding. + * + * @internal + */ +class ChunkedDecoder extends EventEmitter implements ReadableStreamInterface +{ + const CRLF = "\r\n"; + const MAX_CHUNK_HEADER_SIZE = 1024; + + private $closed = false; + private $input; + private $buffer = ''; + private $chunkSize = 0; + private $transferredSize = 0; + private $headerCompleted = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->buffer = ''; + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new Exception('Unexpected end event')); + } + } + + /** @internal */ + public function handleError(Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + while ($this->buffer !== '') { + if (!$this->headerCompleted) { + $positionCrlf = \strpos($this->buffer, static::CRLF); + + if ($positionCrlf === false) { + // Header shouldn't be bigger than 1024 bytes + if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) { + $this->handleError(new Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes')); + } + return; + } + + $header = \strtolower((string)\substr($this->buffer, 0, $positionCrlf)); + $hexValue = $header; + + if (\strpos($header, ';') !== false) { + $array = \explode(';', $header); + $hexValue = $array[0]; + } + + if ($hexValue !== '') { + $hexValue = \ltrim(\trim($hexValue), "0"); + if ($hexValue === '') { + $hexValue = "0"; + } + } + + $this->chunkSize = @\hexdec($hexValue); + if (!\is_int($this->chunkSize) || \dechex($this->chunkSize) !== $hexValue) { + $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); + return; + } + + $this->buffer = (string)\substr($this->buffer, $positionCrlf + 2); + $this->headerCompleted = true; + if ($this->buffer === '') { + return; + } + } + + $chunk = (string)\substr($this->buffer, 0, $this->chunkSize - $this->transferredSize); + + if ($chunk !== '') { + $this->transferredSize += \strlen($chunk); + $this->emit('data', array($chunk)); + $this->buffer = (string)\substr($this->buffer, \strlen($chunk)); + } + + $positionCrlf = \strpos($this->buffer, static::CRLF); + + if ($positionCrlf === 0) { + if ($this->chunkSize === 0) { + $this->emit('end'); + $this->close(); + return; + } + $this->chunkSize = 0; + $this->headerCompleted = false; + $this->transferredSize = 0; + $this->buffer = (string)\substr($this->buffer, 2); + } elseif ($this->chunkSize === 0) { + // end chunk received, skip all trailer data + $this->buffer = (string)\substr($this->buffer, $positionCrlf); + } + + if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { + // the first 2 characters are not CRLF, send error event + $this->handleError(new Exception('Chunk does not end with a CRLF')); + return; + } + + if ($positionCrlf !== 0 && \strlen($this->buffer) < 2) { + // No CRLF found, wait for additional data which could be a CRLF + return; + } + } + } +} diff --git a/vendor/react/http/src/Io/ChunkedEncoder.php b/vendor/react/http/src/Io/ChunkedEncoder.php new file mode 100644 index 0000000..c84ef54 --- /dev/null +++ b/vendor/react/http/src/Io/ChunkedEncoder.php @@ -0,0 +1,92 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Encodes given payload stream with "Transfer-Encoding: chunked" and emits encoded data + * + * This is used internally to encode outgoing requests with this encoding. + * + * @internal + */ +class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($data !== '') { + $this->emit('data', array( + \dechex(\strlen($data)) . "\r\n" . $data . "\r\n" + )); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('data', array("0\r\n\r\n")); + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/vendor/react/http/src/Io/CloseProtectionStream.php b/vendor/react/http/src/Io/CloseProtectionStream.php new file mode 100644 index 0000000..2e1ed6e --- /dev/null +++ b/vendor/react/http/src/Io/CloseProtectionStream.php @@ -0,0 +1,111 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Protects a given stream from actually closing and only discards its incoming data instead. + * + * This is used internally to prevent the underlying connection from closing, so + * that we can still send back a response over the same stream. + * + * @internal + * */ +class CloseProtectionStream extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + private $paused = false; + + /** + * @param ReadableStreamInterface $input stream that will be discarded instead of closing it on an 'close' event. + */ + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->paused = true; + $this->input->pause(); + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + // stop listening for incoming events + $this->input->removeListener('data', array($this, 'handleData')); + $this->input->removeListener('error', array($this, 'handleError')); + $this->input->removeListener('end', array($this, 'handleEnd')); + $this->input->removeListener('close', array($this, 'close')); + + // resume the stream to ensure we discard everything from incoming connection + if ($this->paused) { + $this->paused = false; + $this->input->resume(); + } + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('end'); + $this->close(); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + } +} diff --git a/vendor/react/http/src/Io/EmptyBodyStream.php b/vendor/react/http/src/Io/EmptyBodyStream.php new file mode 100644 index 0000000..5056219 --- /dev/null +++ b/vendor/react/http/src/Io/EmptyBodyStream.php @@ -0,0 +1,142 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Bridge between an empty StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP + * + * This class is used in the server to represent an empty body stream of an + * incoming response from the client. This is similar to the `HttpBodyStream`, + * but is specifically designed for the common case of having an empty message + * body. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `StreamInterface` and `ReadableStreamInterface` for more + * details. + * + * @see HttpBodyStream + * @see StreamInterface + * @see ReadableStreamInterface + * @internal + */ +class EmptyBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface +{ + private $closed = false; + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + // NOOP + } + + public function resume() + { + // NOOP + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return 0; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } +} diff --git a/vendor/react/http/src/Io/HttpBodyStream.php b/vendor/react/http/src/Io/HttpBodyStream.php new file mode 100644 index 0000000..25d15a1 --- /dev/null +++ b/vendor/react/http/src/Io/HttpBodyStream.php @@ -0,0 +1,182 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Bridge between StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP + * + * This class is used in the server to stream the body of an incoming response + * from the client. This allows us to stream big amounts of data without having + * to buffer this data. Similarly, this used to stream the body of an outgoing + * request body to the client. The data will be sent directly to the client. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `StreamInterface` and `ReadableStreamInterface` for more + * details. + * + * @see StreamInterface + * @see ReadableStreamInterface + * @internal + */ +class HttpBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface +{ + public $input; + private $closed = false; + private $size; + + /** + * @param ReadableStreamInterface $input Stream data from $stream as a body of a PSR-7 object4 + * @param int|null $size size of the data body + */ + public function __construct(ReadableStreamInterface $input, $size) + { + $this->input = $input; + $this->size = $size; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return $this->size; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return null; + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/vendor/react/http/src/Io/IniUtil.php b/vendor/react/http/src/Io/IniUtil.php new file mode 100644 index 0000000..612aae2 --- /dev/null +++ b/vendor/react/http/src/Io/IniUtil.php @@ -0,0 +1,48 @@ +<?php + +namespace React\Http\Io; + +/** + * @internal + */ +final class IniUtil +{ + /** + * Convert a ini like size to a numeric size in bytes. + * + * @param string $size + * @return int + */ + public static function iniSizeToBytes($size) + { + if (\is_numeric($size)) { + return (int)$size; + } + + $suffix = \strtoupper(\substr($size, -1)); + $strippedSize = \substr($size, 0, -1); + + if (!\is_numeric($strippedSize)) { + throw new \InvalidArgumentException("$size is not a valid ini size"); + } + + if ($strippedSize <= 0) { + throw new \InvalidArgumentException("Expect $size to be higher isn't zero or lower"); + } + + if ($suffix === 'K') { + return $strippedSize * 1024; + } + if ($suffix === 'M') { + return $strippedSize * 1024 * 1024; + } + if ($suffix === 'G') { + return $strippedSize * 1024 * 1024 * 1024; + } + if ($suffix === 'T') { + return $strippedSize * 1024 * 1024 * 1024 * 1024; + } + + return (int)$size; + } +} diff --git a/vendor/react/http/src/Io/LengthLimitedStream.php b/vendor/react/http/src/Io/LengthLimitedStream.php new file mode 100644 index 0000000..bc64c54 --- /dev/null +++ b/vendor/react/http/src/Io/LengthLimitedStream.php @@ -0,0 +1,108 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Limits the amount of data the given stream can emit + * + * This is used internally to limit the size of the underlying connection stream + * to the size defined by the "Content-Length" header of the incoming request. + * + * @internal + */ +class LengthLimitedStream extends EventEmitter implements ReadableStreamInterface +{ + private $stream; + private $closed = false; + private $transferredLength = 0; + private $maxLength; + + public function __construct(ReadableStreamInterface $stream, $maxLength) + { + $this->stream = $stream; + $this->maxLength = $maxLength; + + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('end', array($this, 'handleEnd')); + $this->stream->on('error', array($this, 'handleError')); + $this->stream->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->stream->isReadable(); + } + + public function pause() + { + $this->stream->pause(); + } + + public function resume() + { + $this->stream->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->stream->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if (($this->transferredLength + \strlen($data)) > $this->maxLength) { + // Only emit data until the value of 'Content-Length' is reached, the rest will be ignored + $data = (string)\substr($data, 0, $this->maxLength - $this->transferredLength); + } + + if ($data !== '') { + $this->transferredLength += \strlen($data); + $this->emit('data', array($data)); + } + + if ($this->transferredLength === $this->maxLength) { + // 'Content-Length' reached, stream will end + $this->emit('end'); + $this->close(); + $this->stream->removeListener('data', array($this, 'handleData')); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new \Exception('Unexpected end event')); + } + } + +} diff --git a/vendor/react/http/src/Io/MiddlewareRunner.php b/vendor/react/http/src/Io/MiddlewareRunner.php new file mode 100644 index 0000000..dedf6ff --- /dev/null +++ b/vendor/react/http/src/Io/MiddlewareRunner.php @@ -0,0 +1,61 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use React\Promise\PromiseInterface; + +/** + * [Internal] Middleware runner to expose an array of middleware request handlers as a single request handler callable + * + * @internal + */ +final class MiddlewareRunner +{ + /** + * @var callable[] + */ + private $middleware; + + /** + * @param callable[] $middleware + */ + public function __construct(array $middleware) + { + $this->middleware = \array_values($middleware); + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface|PromiseInterface<ResponseInterface> + * @throws \Exception + */ + public function __invoke(ServerRequestInterface $request) + { + if (empty($this->middleware)) { + throw new \RuntimeException('No middleware to run'); + } + + return $this->call($request, 0); + } + + /** @internal */ + public function call(ServerRequestInterface $request, $position) + { + // final request handler will be invoked without a next handler + if (!isset($this->middleware[$position + 1])) { + $handler = $this->middleware[$position]; + return $handler($request); + } + + $that = $this; + $next = function (ServerRequestInterface $request) use ($that, $position) { + return $that->call($request, $position + 1); + }; + + // invoke middleware request handler with next handler + $handler = $this->middleware[$position]; + return $handler($request, $next); + } +} diff --git a/vendor/react/http/src/Io/MultipartParser.php b/vendor/react/http/src/Io/MultipartParser.php new file mode 100644 index 0000000..536694f --- /dev/null +++ b/vendor/react/http/src/Io/MultipartParser.php @@ -0,0 +1,328 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\ServerRequestInterface; + +/** + * [Internal] Parses a string body with "Content-Type: multipart/form-data" into structured data + * + * This is use internally to parse incoming request bodies into structured data + * that resembles PHP's `$_POST` and `$_FILES` superglobals. + * + * @internal + * @link https://tools.ietf.org/html/rfc7578 + * @link https://tools.ietf.org/html/rfc2046#section-5.1.1 + */ +final class MultipartParser +{ + /** + * @var ServerRequestInterface|null + */ + private $request; + + /** + * @var int|null + */ + private $maxFileSize; + + /** + * ini setting "max_input_vars" + * + * Does not exist in PHP < 5.3.9 or HHVM, so assume PHP's default 1000 here. + * + * @var int + * @link http://php.net/manual/en/info.configuration.php#ini.max-input-vars + */ + private $maxInputVars = 1000; + + /** + * ini setting "max_input_nesting_level" + * + * Does not exist in HHVM, but assumes hard coded to 64 (PHP's default). + * + * @var int + * @link http://php.net/manual/en/info.configuration.php#ini.max-input-nesting-level + */ + private $maxInputNestingLevel = 64; + + /** + * ini setting "upload_max_filesize" + * + * @var int + */ + private $uploadMaxFilesize; + + /** + * ini setting "max_file_uploads" + * + * Additionally, setting "file_uploads = off" effectively sets this to zero. + * + * @var int + */ + private $maxFileUploads; + + private $postCount = 0; + private $filesCount = 0; + private $emptyCount = 0; + + /** + * @param int|string|null $uploadMaxFilesize + * @param int|null $maxFileUploads + */ + public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) + { + $var = \ini_get('max_input_vars'); + if ($var !== false) { + $this->maxInputVars = (int)$var; + } + $var = \ini_get('max_input_nesting_level'); + if ($var !== false) { + $this->maxInputNestingLevel = (int)$var; + } + + if ($uploadMaxFilesize === null) { + $uploadMaxFilesize = \ini_get('upload_max_filesize'); + } + + $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); + $this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads; + } + + public function parse(ServerRequestInterface $request) + { + $contentType = $request->getHeaderLine('content-type'); + if(!\preg_match('/boundary="?(.*?)"?$/', $contentType, $matches)) { + return $request; + } + + $this->request = $request; + $this->parseBody('--' . $matches[1], (string)$request->getBody()); + + $request = $this->request; + $this->request = null; + $this->postCount = 0; + $this->filesCount = 0; + $this->emptyCount = 0; + $this->maxFileSize = null; + + return $request; + } + + private function parseBody($boundary, $buffer) + { + $len = \strlen($boundary); + + // ignore everything before initial boundary (SHOULD be empty) + $start = \strpos($buffer, $boundary . "\r\n"); + + while ($start !== false) { + // search following boundary (preceded by newline) + // ignore last if not followed by boundary (SHOULD end with "--") + $start += $len + 2; + $end = \strpos($buffer, "\r\n" . $boundary, $start); + if ($end === false) { + break; + } + + // parse one part and continue searching for next + $this->parsePart(\substr($buffer, $start, $end - $start)); + $start = $end; + } + } + + private function parsePart($chunk) + { + $pos = \strpos($chunk, "\r\n\r\n"); + if ($pos === false) { + return; + } + + $headers = $this->parseHeaders((string)substr($chunk, 0, $pos)); + $body = (string)\substr($chunk, $pos + 4); + + if (!isset($headers['content-disposition'])) { + return; + } + + $name = $this->getParameterFromHeader($headers['content-disposition'], 'name'); + if ($name === null) { + return; + } + + $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); + if ($filename !== null) { + $this->parseFile( + $name, + $filename, + isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, + $body + ); + } else { + $this->parsePost($name, $body); + } + } + + private function parseFile($name, $filename, $contentType, $contents) + { + $file = $this->parseUploadedFile($filename, $contentType, $contents); + if ($file === null) { + return; + } + + $this->request = $this->request->withUploadedFiles($this->extractPost( + $this->request->getUploadedFiles(), + $name, + $file + )); + } + + private function parseUploadedFile($filename, $contentType, $contents) + { + $size = \strlen($contents); + + // no file selected (zero size and empty filename) + if ($size === 0 && $filename === '') { + // ignore excessive number of empty file uploads + if (++$this->emptyCount + $this->filesCount > $this->maxInputVars) { + return; + } + + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_NO_FILE, + $filename, + $contentType + ); + } + + // ignore excessive number of file uploads + if (++$this->filesCount > $this->maxFileUploads) { + return; + } + + // file exceeds "upload_max_filesize" ini setting + if ($size > $this->uploadMaxFilesize) { + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_INI_SIZE, + $filename, + $contentType + ); + } + + // file exceeds MAX_FILE_SIZE value + if ($this->maxFileSize !== null && $size > $this->maxFileSize) { + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_FORM_SIZE, + $filename, + $contentType + ); + } + + return new UploadedFile( + new BufferedBody($contents), + $size, + \UPLOAD_ERR_OK, + $filename, + $contentType + ); + } + + private function parsePost($name, $value) + { + // ignore excessive number of post fields + if (++$this->postCount > $this->maxInputVars) { + return; + } + + $this->request = $this->request->withParsedBody($this->extractPost( + $this->request->getParsedBody(), + $name, + $value + )); + + if (\strtoupper($name) === 'MAX_FILE_SIZE') { + $this->maxFileSize = (int)$value; + + if ($this->maxFileSize === 0) { + $this->maxFileSize = null; + } + } + } + + private function parseHeaders($header) + { + $headers = array(); + + foreach (\explode("\r\n", \trim($header)) as $line) { + $parts = \explode(':', $line, 2); + if (!isset($parts[1])) { + continue; + } + + $key = \strtolower(trim($parts[0])); + $values = \explode(';', $parts[1]); + $values = \array_map('trim', $values); + $headers[$key] = $values; + } + + return $headers; + } + + private function getParameterFromHeader(array $header, $parameter) + { + foreach ($header as $part) { + if (\preg_match('/' . $parameter . '="?(.*?)"?$/', $part, $matches)) { + return $matches[1]; + } + } + + return null; + } + + private function extractPost($postFields, $key, $value) + { + $chunks = \explode('[', $key); + if (\count($chunks) == 1) { + $postFields[$key] = $value; + return $postFields; + } + + // ignore this key if maximum nesting level is exceeded + if (isset($chunks[$this->maxInputNestingLevel])) { + return $postFields; + } + + $chunkKey = \rtrim($chunks[0], ']'); + $parent = &$postFields; + for ($i = 1; isset($chunks[$i]); $i++) { + $previousChunkKey = $chunkKey; + + if ($previousChunkKey === '') { + $parent[] = array(); + \end($parent); + $parent = &$parent[\key($parent)]; + } else { + if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { + $parent[$previousChunkKey] = array(); + } + $parent = &$parent[$previousChunkKey]; + } + + $chunkKey = \rtrim($chunks[$i], ']'); + } + + if ($chunkKey === '') { + $parent[] = $value; + } else { + $parent[$chunkKey] = $value; + } + + return $postFields; + } +} diff --git a/vendor/react/http/src/Io/PauseBufferStream.php b/vendor/react/http/src/Io/PauseBufferStream.php new file mode 100644 index 0000000..fb5ed45 --- /dev/null +++ b/vendor/react/http/src/Io/PauseBufferStream.php @@ -0,0 +1,188 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Pauses a given stream and buffers all events while paused + * + * This class is used to buffer all events that happen on a given stream while + * it is paused. This allows you to pause a stream and no longer watch for any + * of its events. Once the stream is resumed, all buffered events will be + * emitted. Explicitly closing the resulting stream clears all buffers. + * + * Note that this is an internal class only and nothing you should usually care + * about. + * + * @see ReadableStreamInterface + * @internal + */ +class PauseBufferStream extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + private $paused = false; + private $dataPaused = ''; + private $endPaused = false; + private $closePaused = false; + private $errorPaused; + private $implicit = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'handleClose')); + } + + /** + * pause and remember this was not explicitly from user control + * + * @internal + */ + public function pauseImplicit() + { + $this->pause(); + $this->implicit = true; + } + + /** + * resume only if this was previously paused implicitly and not explicitly from user control + * + * @internal + */ + public function resumeImplicit() + { + if ($this->implicit) { + $this->resume(); + } + } + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->input->pause(); + $this->paused = true; + $this->implicit = false; + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + $this->implicit = false; + + if ($this->dataPaused !== '') { + $this->emit('data', array($this->dataPaused)); + $this->dataPaused = ''; + } + + if ($this->errorPaused) { + $this->emit('error', array($this->errorPaused)); + return $this->close(); + } + + if ($this->endPaused) { + $this->endPaused = false; + $this->emit('end'); + return $this->close(); + } + + if ($this->closePaused) { + $this->closePaused = false; + return $this->close(); + } + + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->dataPaused = ''; + $this->endPaused = $this->closePaused = false; + $this->errorPaused = null; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($this->paused) { + $this->dataPaused .= $data; + return; + } + + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + if ($this->paused) { + $this->errorPaused = $e; + return; + } + + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if ($this->paused) { + $this->endPaused = true; + return; + } + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** @internal */ + public function handleClose() + { + if ($this->paused) { + $this->closePaused = true; + return; + } + + $this->close(); + } +} diff --git a/vendor/react/http/src/Io/ReadableBodyStream.php b/vendor/react/http/src/Io/ReadableBodyStream.php new file mode 100644 index 0000000..daef45f --- /dev/null +++ b/vendor/react/http/src/Io/ReadableBodyStream.php @@ -0,0 +1,153 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * @internal + */ +class ReadableBodyStream extends EventEmitter implements ReadableStreamInterface, StreamInterface +{ + private $input; + private $position = 0; + private $size; + private $closed = false; + + public function __construct(ReadableStreamInterface $input, $size = null) + { + $this->input = $input; + $this->size = $size; + + $that = $this; + $pos =& $this->position; + $input->on('data', function ($data) use ($that, &$pos, $size) { + $that->emit('data', array($data)); + + $pos += \strlen($data); + if ($size !== null && $pos >= $size) { + $that->handleEnd(); + } + }); + $input->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + $that->close(); + }); + $input->on('end', array($that, 'handleEnd')); + $input->on('close', array($that, 'close')); + } + + public function close() + { + if (!$this->closed) { + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function eof() + { + return !$this->isReadable(); + } + + public function __toString() + { + return ''; + } + + public function detach() + { + throw new \BadMethodCallException(); + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + throw new \BadMethodCallException(); + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + public function rewind() + { + throw new \BadMethodCallException(); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \BadMethodCallException(); + } + + public function read($length) + { + throw new \BadMethodCallException(); + } + + public function getContents() + { + throw new \BadMethodCallException(); + } + + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } + + /** @internal */ + public function handleEnd() + { + if ($this->position !== $this->size && $this->size !== null) { + $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + } else { + $this->emit('end'); + } + + $this->close(); + } +} diff --git a/vendor/react/http/src/Io/RequestHeaderParser.php b/vendor/react/http/src/Io/RequestHeaderParser.php new file mode 100644 index 0000000..e5554c4 --- /dev/null +++ b/vendor/react/http/src/Io/RequestHeaderParser.php @@ -0,0 +1,281 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Message\Response; +use React\Http\Message\ServerRequest; +use React\Socket\ConnectionInterface; +use Exception; + +/** + * [Internal] Parses an incoming request header from an input stream + * + * This is used internally to parse the request header from the connection and + * then process the remaining connection as the request body. + * + * @event headers + * @event error + * + * @internal + */ +class RequestHeaderParser extends EventEmitter +{ + private $maxSize = 8192; + + public function handle(ConnectionInterface $conn) + { + $buffer = ''; + $maxSize = $this->maxSize; + $that = $this; + $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) { + // append chunk of data to buffer and look for end of request headers + $buffer .= $data; + $endOfHeader = \strpos($buffer, "\r\n\r\n"); + + // reject request if buffer size is exceeded + if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) { + $conn->removeListener('data', $fn); + $fn = null; + + $that->emit('error', array( + new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + $conn + )); + return; + } + + // ignore incomplete requests + if ($endOfHeader === false) { + return; + } + + // request headers received => try to parse request + $conn->removeListener('data', $fn); + $fn = null; + + try { + $request = $that->parseRequest( + (string)\substr($buffer, 0, $endOfHeader + 2), + $conn->getRemoteAddress(), + $conn->getLocalAddress() + ); + } catch (Exception $exception) { + $buffer = ''; + $that->emit('error', array( + $exception, + $conn + )); + return; + } + + $contentLength = 0; + if ($request->hasHeader('Transfer-Encoding')) { + $contentLength = null; + } elseif ($request->hasHeader('Content-Length')) { + $contentLength = (int)$request->getHeaderLine('Content-Length'); + } + + if ($contentLength === 0) { + // happy path: request body is known to be empty + $stream = new EmptyBodyStream(); + $request = $request->withBody($stream); + } else { + // otherwise body is present => delimit using Content-Length or ChunkedDecoder + $stream = new CloseProtectionStream($conn); + if ($contentLength !== null) { + $stream = new LengthLimitedStream($stream, $contentLength); + } else { + $stream = new ChunkedDecoder($stream); + } + + $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); + } + + $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; + $buffer = ''; + $that->emit('headers', array($request, $conn)); + + if ($bodyBuffer !== '') { + $conn->emit('data', array($bodyBuffer)); + } + + // happy path: request body is known to be empty => immediately end stream + if ($contentLength === 0) { + $stream->emit('end'); + $stream->close(); + } + }); + } + + /** + * @param string $headers buffer string containing request headers only + * @param ?string $remoteSocketUri + * @param ?string $localSocketUri + * @return ServerRequestInterface + * @throws \InvalidArgumentException + * @internal + */ + public function parseRequest($headers, $remoteSocketUri, $localSocketUri) + { + // additional, stricter safe-guard for request line + // because request parser doesn't properly cope with invalid ones + $start = array(); + if (!\preg_match('#^(?<method>[^ ]+) (?<target>[^ ]+) HTTP/(?<version>\d\.\d)#m', $headers, $start)) { + throw new \InvalidArgumentException('Unable to parse invalid request-line'); + } + + // only support HTTP/1.1 and HTTP/1.0 requests + if ($start['version'] !== '1.1' && $start['version'] !== '1.0') { + throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED); + } + + // match all request header fields into array, thanks to @kelunik for checking the HTTP specs and coming up with this regex + $matches = array(); + $n = \preg_match_all('/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m', $headers, $matches, \PREG_SET_ORDER); + + // check number of valid header fields matches number of lines + request line + if (\substr_count($headers, "\n") !== $n + 1) { + throw new \InvalidArgumentException('Unable to parse invalid request header fields'); + } + + // format all header fields into associative array + $host = null; + $fields = array(); + foreach ($matches as $match) { + $fields[$match[1]][] = $match[2]; + + // match `Host` request header + if ($host === null && \strtolower($match[1]) === 'host') { + $host = $match[2]; + } + } + + // create new obj implementing ServerRequestInterface by preserving all + // previous properties and restoring original request-target + $serverParams = array( + 'REQUEST_TIME' => \time(), + 'REQUEST_TIME_FLOAT' => \microtime(true) + ); + + // scheme is `http` unless TLS is used + $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); + if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { + $scheme = 'https://'; + $serverParams['HTTPS'] = 'on'; + } else { + $scheme = 'http://'; + } + + // default host if unset comes from local socket address or defaults to localhost + $hasHost = $host !== null; + if ($host === null) { + $host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1'; + } + + if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { + // support asterisk-form for `OPTIONS *` request line only + $uri = $scheme . $host; + } elseif ($start['method'] === 'CONNECT') { + $parts = \parse_url('tcp://' . $start['target']); + + // check this is a valid authority-form request-target (host:port) + if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) { + throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); + } + $uri = $scheme . $start['target']; + } else { + // support absolute-form or origin-form for proxy requests + if ($start['target'][0] === '/') { + $uri = $scheme . $host . $start['target']; + } else { + // ensure absolute-form request-target contains a valid URI + $parts = \parse_url($start['target']); + + // make sure value contains valid host component (IP or hostname), but no fragment + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { + throw new \InvalidArgumentException('Invalid absolute-form request-target'); + } + + $uri = $start['target']; + } + } + + // apply REMOTE_ADDR and REMOTE_PORT if source address is known + // address should always be known, unless this is over Unix domain sockets (UDS) + if ($remoteSocketUri !== null) { + $remoteAddress = \parse_url($remoteSocketUri); + $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; + $serverParams['REMOTE_PORT'] = $remoteAddress['port']; + } + + // apply SERVER_ADDR and SERVER_PORT if server address is known + // address should always be known, even for Unix domain sockets (UDS) + // but skip UDS as it doesn't have a concept of host/port. + if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) { + $serverParams['SERVER_ADDR'] = $localParts['host']; + $serverParams['SERVER_PORT'] = $localParts['port']; + } + + $request = new ServerRequest( + $start['method'], + $uri, + $fields, + '', + $start['version'], + $serverParams + ); + + // only assign request target if it is not in origin-form (happy path for most normal requests) + if ($start['target'][0] !== '/') { + $request = $request->withRequestTarget($start['target']); + } + + if ($hasHost) { + // Optional Host request header value MUST be valid (host and optional port) + $parts = \parse_url('http://' . $request->getHeaderLine('Host')); + + // make sure value contains valid host component (IP or hostname) + if (!$parts || !isset($parts['scheme'], $parts['host'])) { + $parts = false; + } + + // make sure value does not contain any other URI component + if (\is_array($parts)) { + unset($parts['scheme'], $parts['host'], $parts['port']); + } + if ($parts === false || $parts) { + throw new \InvalidArgumentException('Invalid Host header value'); + } + } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') { + // require Host request header for HTTP/1.1 (except for CONNECT method) + throw new \InvalidArgumentException('Missing required Host request header'); + } elseif (!$hasHost) { + // remove default Host request header for HTTP/1.0 when not explicitly given + $request = $request->withoutHeader('Host'); + } + + // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers + if ($request->hasHeader('Transfer-Encoding')) { + if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { + throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED); + } + + // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time + // as per https://tools.ietf.org/html/rfc7230#section-3.3.3 + if ($request->hasHeader('Content-Length')) { + throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST); + } + } elseif ($request->hasHeader('Content-Length')) { + $string = $request->getHeaderLine('Content-Length'); + + if ((string)(int)$string !== $string) { + // Content-Length value is not an integer or not a single integer + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); + } + } + + return $request; + } +} diff --git a/vendor/react/http/src/Io/Sender.php b/vendor/react/http/src/Io/Sender.php new file mode 100644 index 0000000..2f04c79 --- /dev/null +++ b/vendor/react/http/src/Io/Sender.php @@ -0,0 +1,160 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use React\EventLoop\LoopInterface; +use React\Http\Client\Client as HttpClient; +use React\Http\Message\Response; +use React\Promise\PromiseInterface; +use React\Promise\Deferred; +use React\Socket\ConnectorInterface; +use React\Stream\ReadableStreamInterface; + +/** + * [Internal] Sends requests and receives responses + * + * The `Sender` is responsible for passing the [`RequestInterface`](#requestinterface) objects to + * the underlying [`HttpClient`](https://github.com/reactphp/http-client) library + * and keeps track of its transmission and converts its reponses back to [`ResponseInterface`](#responseinterface) objects. + * + * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) + * and the default [`Connector`](https://github.com/reactphp/socket-client) and [DNS `Resolver`](https://github.com/reactphp/dns). + * + * The `Sender` class mostly exists in order to abstract changes on the underlying + * components away from this package in order to provide backwards and forwards + * compatibility. + * + * @internal You SHOULD NOT rely on this API, it is subject to change without prior notice! + * @see Browser + */ +class Sender +{ + /** + * create a new default sender attached to the given event loop + * + * This method is used internally to create the "default sender". + * + * You may also use this method if you need custom DNS or connector + * settings. You can use this method manually like this: + * + * ```php + * $connector = new \React\Socket\Connector(array(), $loop); + * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); + * ``` + * + * @param LoopInterface $loop + * @param ConnectorInterface|null $connector + * @return self + */ + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null) + { + return new self(new HttpClient($loop, $connector)); + } + + private $http; + + /** + * [internal] Instantiate Sender + * + * @param HttpClient $http + * @internal + */ + public function __construct(HttpClient $http) + { + $this->http = $http; + } + + /** + * + * @internal + * @param RequestInterface $request + * @return PromiseInterface Promise<ResponseInterface, Exception> + */ + public function send(RequestInterface $request) + { + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size !== 0) { + // automatically assign a "Content-Length" request header if the body size is known and non-empty + $request = $request->withHeader('Content-Length', (string)$size); + } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + // only assign a "Content-Length: 0" request header if the body is expected for certain methods + $request = $request->withHeader('Content-Length', '0'); + } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) { + // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown + $request = $request->withHeader('Transfer-Encoding', 'chunked'); + } else { + // do not use chunked encoding if size is known or if this is an empty request body + $size = 0; + } + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + + $deferred = new Deferred(function ($_, $reject) use ($requestStream) { + // close request stream if request is cancelled + $reject(new \RuntimeException('Request cancelled')); + $requestStream->close(); + }); + + $requestStream->on('error', function($error) use ($deferred) { + $deferred->reject($error); + }); + + $requestStream->on('response', function (ResponseInterface $response, ReadableStreamInterface $body) use ($deferred, $request) { + $length = null; + $code = $response->getStatusCode(); + if ($request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $body = new ChunkedDecoder($body); + } elseif ($response->hasHeader('Content-Length')) { + $length = (int) $response->getHeaderLine('Content-Length'); + } + + $deferred->resolve($response->withBody(new ReadableBodyStream($body, $length))); + }); + + if ($body instanceof ReadableStreamInterface) { + if ($body->isReadable()) { + // length unknown => apply chunked transfer-encoding + if ($size === null) { + $body = new ChunkedEncoder($body); + } + + // pipe body into request stream + // add dummy write to immediately start request even if body does not emit any data yet + $body->pipe($requestStream); + $requestStream->write(''); + + $body->on('close', $close = function () use ($deferred, $requestStream) { + $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); + $requestStream->close(); + }); + $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { + $body->removeListener('close', $close); + $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); + $requestStream->close(); + }); + $body->on('end', function () use ($close, $body) { + $body->removeListener('close', $close); + }); + } else { + // stream is not readable => end request without body + $requestStream->end(); + } + } else { + // body is fully buffered => write as one chunk + $requestStream->end((string)$body); + } + + return $deferred->promise(); + } +} diff --git a/vendor/react/http/src/Io/StreamingServer.php b/vendor/react/http/src/Io/StreamingServer.php new file mode 100644 index 0000000..7818f0b --- /dev/null +++ b/vendor/react/http/src/Io/StreamingServer.php @@ -0,0 +1,383 @@ +<?php + +namespace React\Http\Io; + +use Evenement\EventEmitter; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use React\EventLoop\LoopInterface; +use React\Http\Message\Response; +use React\Http\Message\ServerRequest; +use React\Promise; +use React\Promise\CancellablePromiseInterface; +use React\Promise\PromiseInterface; +use React\Socket\ConnectionInterface; +use React\Socket\ServerInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; + +/** + * The internal `StreamingServer` class is responsible for handling incoming connections and then + * processing each incoming HTTP request. + * + * Unlike the [`HttpServer`](#httpserver) class, it does not buffer and parse the incoming + * HTTP request body by default. This means that the request handler will be + * invoked with a streaming request body. Once the request headers have been + * received, it will invoke the request handler function. This request handler + * function needs to be passed to the constructor and will be invoked with the + * respective [request](#request) object and expects a [response](#response) + * object in return: + * + * ```php + * $server = new StreamingServer($loop, function (ServerRequestInterface $request) { + * return new Response( + * Response::STATUS_OK, + * array( + * 'Content-Type' => 'text/plain' + * ), + * "Hello World!\n" + * ); + * }); + * ``` + * + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#request) chapter for more details. + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#response) chapter for more details. + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method + * as described in the following chapter. In its most simple form, you can attach + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $server = new StreamingServer($loop, $handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); + * $server->listen($socket); + * ``` + * + * See also the [`listen()`](#listen) method and the [first example](examples) for more details. + * + * The `StreamingServer` class is considered advanced usage and unless you know + * what you're doing, you're recommended to use the [`HttpServer`](#httpserver) class + * instead. The `StreamingServer` class is specifically designed to help with + * more advanced use cases where you want to have full control over consuming + * the incoming HTTP request body and concurrency settings. + * + * In particular, this class does not buffer and parse the incoming HTTP request + * in memory. It will invoke the request handler function once the HTTP request + * headers have been received, i.e. before receiving the potentially much larger + * HTTP request body. This means the [request](#request) passed to your request + * handler function may not be fully compatible with PSR-7. See also + * [streaming request](#streaming-request) below for more details. + * + * @see \React\Http\HttpServer + * @see \React\Http\Message\Response + * @see self::listen() + * @internal + */ +final class StreamingServer extends EventEmitter +{ + private $callback; + private $parser; + private $loop; + + /** + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param LoopInterface $loop + * @param callable $requestHandler + * @see self::listen() + */ + public function __construct(LoopInterface $loop, $requestHandler) + { + if (!\is_callable($requestHandler)) { + throw new \InvalidArgumentException('Invalid request handler given'); + } + + $this->loop = $loop; + + $this->callback = $requestHandler; + $this->parser = new RequestHeaderParser(); + + $that = $this; + $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { + $that->handleRequest($conn, $request); + }); + + $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) { + $that->emit('error', array($e)); + + // parsing failed => assume dummy request and send appropriate error + $that->writeError( + $conn, + $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, + new ServerRequest('GET', '/') + ); + }); + } + + /** + * Starts listening for HTTP requests on the given socket server instance + * + * @param ServerInterface $socket + * @see \React\Http\HttpServer::listen() + */ + public function listen(ServerInterface $socket) + { + $socket->on('connection', array($this->parser, 'handle')); + } + + /** @internal */ + public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) + { + if ($request->getProtocolVersion() !== '1.0' && '100-continue' === \strtolower($request->getHeaderLine('Expect'))) { + $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); + } + + // execute request handler callback + $callback = $this->callback; + try { + $response = $callback($request); + } catch (\Exception $error) { + // request handler callback throws an Exception + $response = Promise\reject($error); + } catch (\Throwable $error) { // @codeCoverageIgnoreStart + // request handler callback throws a PHP7+ Error + $response = Promise\reject($error); // @codeCoverageIgnoreEnd + } + + // cancel pending promise once connection closes + if ($response instanceof CancellablePromiseInterface) { + $conn->on('close', function () use ($response) { + $response->cancel(); + }); + } + + // happy path: response returned, handle and return immediately + if ($response instanceof ResponseInterface) { + return $this->handleResponse($conn, $request, $response); + } + + // did not return a promise? this is an error, convert into one for rejection below. + if (!$response instanceof PromiseInterface) { + $response = Promise\resolve($response); + } + + $that = $this; + $response->then( + function ($response) use ($that, $conn, $request) { + if (!$response instanceof ResponseInterface) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; + $message = \sprintf($message, \is_object($response) ? \get_class($response) : \gettype($response)); + $exception = new \RuntimeException($message); + + $that->emit('error', array($exception)); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + } + $that->handleResponse($conn, $request, $response); + }, + function ($error) use ($that, $conn, $request) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; + $message = \sprintf($message, \is_object($error) ? \get_class($error) : \gettype($error)); + + $previous = null; + + if ($error instanceof \Throwable || $error instanceof \Exception) { + $previous = $error; + } + + $exception = new \RuntimeException($message, 0, $previous); + + $that->emit('error', array($exception)); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + } + ); + } + + /** @internal */ + public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request) + { + $response = new Response( + $code, + array( + 'Content-Type' => 'text/plain', + 'Connection' => 'close' // we do not want to keep the connection open after an error + ), + 'Error ' . $code + ); + + // append reason phrase to response body if known + $reason = $response->getReasonPhrase(); + if ($reason !== '') { + $body = $response->getBody(); + $body->seek(0, SEEK_END); + $body->write(': ' . $reason); + } + + $this->handleResponse($conn, $request, $response); + } + + + /** @internal */ + public function handleResponse(ConnectionInterface $connection, ServerRequestInterface $request, ResponseInterface $response) + { + // return early and close response body if connection is already closed + $body = $response->getBody(); + if (!$connection->isWritable()) { + $body->close(); + return; + } + + $code = $response->getStatusCode(); + $method = $request->getMethod(); + + // assign HTTP protocol version from request automatically + $version = $request->getProtocolVersion(); + $response = $response->withProtocolVersion($version); + + // assign default "Server" header automatically + if (!$response->hasHeader('Server')) { + $response = $response->withHeader('Server', 'ReactPHP/1'); + } elseif ($response->getHeaderLine('Server') === ''){ + $response = $response->withoutHeader('Server'); + } + + // assign default "Date" header from current time automatically + if (!$response->hasHeader('Date')) { + // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s') . ' GMT'); + } elseif ($response->getHeaderLine('Date') === ''){ + $response = $response->withoutHeader('Date'); + } + + // assign "Content-Length" header automatically + $chunked = false; + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { + // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header + $response = $response->withoutHeader('Content-Length'); + } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty + } elseif ($body->getSize() !== null) { + // assign Content-Length header when using a "normal" buffered body string + $response = $response->withHeader('Content-Length', (string)$body->getSize()); + } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses + $chunked = true; + } + + // assign "Transfer-Encoding" header automatically + if ($chunked) { + $response = $response->withHeader('Transfer-Encoding', 'chunked'); + } else { + // remove any Transfer-Encoding headers unless automatically enabled above + $response = $response->withoutHeader('Transfer-Encoding'); + } + + // assign "Connection" header automatically + $persist = false; + if ($code === Response::STATUS_SWITCHING_PROTOCOLS) { + // 101 (Switching Protocols) response uses Connection: upgrade header + // This implies that this stream now uses another protocol and we + // may not persist this connection for additional requests. + $response = $response->withHeader('Connection', 'upgrade'); + } elseif (\strtolower($request->getHeaderLine('Connection')) === 'close' || \strtolower($response->getHeaderLine('Connection')) === 'close') { + // obey explicit "Connection: close" request header or response header if present + $response = $response->withHeader('Connection', 'close'); + } elseif ($version === '1.1') { + // HTTP/1.1 assumes persistent connection support by default, so we don't need to inform client + $persist = true; + } elseif (strtolower($request->getHeaderLine('Connection')) === 'keep-alive') { + // obey explicit "Connection: keep-alive" request header and inform client + $persist = true; + $response = $response->withHeader('Connection', 'keep-alive'); + } else { + // remove any Connection headers unless automatically enabled above + $response = $response->withoutHeader('Connection'); + } + + // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream + // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream + if (($code === Response::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if ($request->getBody()->isReadable()) { + // request is still streaming => wait for request close before forwarding following data from connection + $request->getBody()->on('close', function () use ($connection, $body) { + if ($body->input->isWritable()) { + $connection->pipe($body->input); + $connection->resume(); + } + }); + } elseif ($body->input->isWritable()) { + // request already closed => forward following data from connection + $connection->pipe($body->input); + $connection->resume(); + } + } + + // build HTTP response header by appending status line and header fields + $headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n"; + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + $headers .= $name . ": " . $value . "\r\n"; + } + } + + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body + // exclude status 101 (Switching Protocols) here for Upgrade request handling above + if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) { + $body->close(); + $body = ''; + } + + // this is a non-streaming response body or the body stream already closed? + if (!$body instanceof ReadableStreamInterface || !$body->isReadable()) { + // add final chunk if a streaming body is already closed and uses `Transfer-Encoding: chunked` + if ($body instanceof ReadableStreamInterface && $chunked) { + $body = "0\r\n\r\n"; + } + + // write response headers and body + $connection->write($headers . "\r\n" . $body); + + // either wait for next request over persistent connection or end connection + if ($persist) { + $this->parser->handle($connection); + } else { + $connection->end(); + } + return; + } + + $connection->write($headers . "\r\n"); + + if ($chunked) { + $body = new ChunkedEncoder($body); + } + + // Close response stream once connection closes. + // Note that this TCP/IP close detection may take some time, + // in particular this may only fire on a later read/write attempt. + $connection->on('close', array($body, 'close')); + + // write streaming body and then wait for next request over persistent connection + if ($persist) { + $body->pipe($connection, array('end' => false)); + $parser = $this->parser; + $body->on('end', function () use ($connection, $parser, $body) { + $connection->removeListener('close', array($body, 'close')); + $parser->handle($connection); + }); + } else { + $body->pipe($connection); + } + } +} diff --git a/vendor/react/http/src/Io/Transaction.php b/vendor/react/http/src/Io/Transaction.php new file mode 100644 index 0000000..7bf7008 --- /dev/null +++ b/vendor/react/http/src/Io/Transaction.php @@ -0,0 +1,303 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; +use RingCentral\Psr7\Request; +use RingCentral\Psr7\Uri; +use React\EventLoop\LoopInterface; +use React\Http\Message\ResponseException; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; + +/** + * @internal + */ +class Transaction +{ + private $sender; + private $loop; + + // context: http.timeout (ini_get('default_socket_timeout'): 60) + private $timeout; + + // context: http.follow_location (true) + private $followRedirects = true; + + // context: http.max_redirects (10) + private $maxRedirects = 10; + + // context: http.ignore_errors (false) + private $obeySuccessCode = true; + + private $streaming = false; + + private $maximumSize = 16777216; // 16 MiB = 2^24 bytes + + public function __construct(Sender $sender, LoopInterface $loop) + { + $this->sender = $sender; + $this->loop = $loop; + } + + /** + * @param array $options + * @return self returns new instance, without modifying existing instance + */ + public function withOptions(array $options) + { + $transaction = clone $this; + foreach ($options as $name => $value) { + if (property_exists($transaction, $name)) { + // restore default value if null is given + if ($value === null) { + $default = new self($this->sender, $this->loop); + $value = $default->$name; + } + + $transaction->$name = $value; + } + } + + return $transaction; + } + + public function send(RequestInterface $request) + { + $deferred = new Deferred(function () use (&$deferred) { + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + + $deferred->numRequests = 0; + + // use timeout from options or default to PHP's default_socket_timeout (60) + $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); + + $loop = $this->loop; + $this->next($request, $deferred)->then( + function (ResponseInterface $response) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->resolve($response); + }, + function ($e) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->reject($e); + } + ); + + if ($timeout < 0) { + return $deferred->promise(); + } + + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $that = $this; + $body->on('close', function () use ($that, $deferred, &$timeout) { + if ($timeout >= 0) { + $that->applyTimeout($deferred, $timeout); + } + }); + } else { + $this->applyTimeout($deferred, $timeout); + } + + return $deferred->promise(); + } + + /** + * @internal + * @param Deferred $deferred + * @param number $timeout + * @return void + */ + public function applyTimeout(Deferred $deferred, $timeout) + { + $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { + $deferred->reject(new \RuntimeException( + 'Request timed out after ' . $timeout . ' seconds' + )); + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + } + + private function next(RequestInterface $request, Deferred $deferred) + { + $this->progress('request', array($request)); + + $that = $this; + ++$deferred->numRequests; + + $promise = $this->sender->send($request); + + if (!$this->streaming) { + $promise = $promise->then(function ($response) use ($deferred, $that) { + return $that->bufferResponse($response, $deferred); + }); + } + + $deferred->pending = $promise; + + return $promise->then( + function (ResponseInterface $response) use ($request, $that, $deferred) { + return $that->onResponse($response, $request, $deferred); + } + ); + } + + /** + * @internal + * @param ResponseInterface $response + * @return PromiseInterface Promise<ResponseInterface, Exception> + */ + public function bufferResponse(ResponseInterface $response, $deferred) + { + $stream = $response->getBody(); + + $size = $stream->getSize(); + if ($size !== null && $size > $this->maximumSize) { + $stream->close(); + return \React\Promise\reject(new \OverflowException( + 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + )); + } + + // body is not streaming => already buffered + if (!$stream instanceof ReadableStreamInterface) { + return \React\Promise\resolve($response); + } + + // buffer stream and resolve with buffered body + $maximumSize = $this->maximumSize; + $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( + function ($body) use ($response) { + return $response->withBody(new BufferedBody($body)); + }, + function ($e) use ($stream, $maximumSize) { + // try to close stream if buffering fails (or is cancelled) + $stream->close(); + + if ($e instanceof \OverflowException) { + $e = new \OverflowException( + 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + ); + } + + throw $e; + } + ); + + $deferred->pending = $promise; + + return $promise; + } + + /** + * @internal + * @param ResponseInterface $response + * @param RequestInterface $request + * @throws ResponseException + * @return ResponseInterface|PromiseInterface + */ + public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) + { + $this->progress('response', array($response, $request)); + + // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled + // @link https://tools.ietf.org/html/rfc7231#section-6.4 + if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { + return $this->onResponseRedirect($response, $request, $deferred); + } + + // only status codes 200-399 are considered to be valid, reject otherwise + if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { + throw new ResponseException($response); + } + + // resolve our initial promise + return $response; + } + + /** + * @param ResponseInterface $response + * @param RequestInterface $request + * @return PromiseInterface + * @throws \RuntimeException + */ + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) + { + // resolve location relative to last request URI + $location = Uri::resolve($request->getUri(), $response->getHeaderLine('Location')); + + $request = $this->makeRedirectRequest($request, $location); + $this->progress('redirect', array($request)); + + if ($deferred->numRequests >= $this->maxRedirects) { + throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); + } + + return $this->next($request, $deferred); + } + + /** + * @param RequestInterface $request + * @param UriInterface $location + * @return RequestInterface + */ + private function makeRedirectRequest(RequestInterface $request, UriInterface $location) + { + $originalHost = $request->getUri()->getHost(); + $request = $request + ->withoutHeader('Host') + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + + // Remove authorization if changing hostnames (but not if just changing ports or protocols). + if ($location->getHost() !== $originalHost) { + $request = $request->withoutHeader('Authorization'); + } + + // naïve approach.. + $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; + + return new Request($method, $location, $request->getHeaders()); + } + + private function progress($name, array $args = array()) + { + return; + + echo $name; + + foreach ($args as $arg) { + echo ' '; + if ($arg instanceof ResponseInterface) { + echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); + } elseif ($arg instanceof RequestInterface) { + echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); + } else { + echo $arg; + } + } + + echo PHP_EOL; + } +} diff --git a/vendor/react/http/src/Io/UploadedFile.php b/vendor/react/http/src/Io/UploadedFile.php new file mode 100644 index 0000000..f2a6c9e --- /dev/null +++ b/vendor/react/http/src/Io/UploadedFile.php @@ -0,0 +1,130 @@ +<?php + +namespace React\Http\Io; + +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UploadedFileInterface; +use InvalidArgumentException; +use RuntimeException; + +/** + * [Internal] Implementation of the PSR-7 `UploadedFileInterface` + * + * This is used internally to represent each incoming file upload. + * + * Note that this is an internal class only and nothing you should usually care + * about. See the `UploadedFileInterface` for more details. + * + * @see UploadedFileInterface + * @internal + */ +final class UploadedFile implements UploadedFileInterface +{ + /** + * @var StreamInterface + */ + private $stream; + + /** + * @var int + */ + private $size; + + /** + * @var int + */ + private $error; + + /** + * @var string + */ + private $filename; + + /** + * @var string + */ + private $mediaType; + + /** + * @param StreamInterface $stream + * @param int $size + * @param int $error + * @param string $filename + * @param string $mediaType + */ + public function __construct(StreamInterface $stream, $size, $error, $filename, $mediaType) + { + $this->stream = $stream; + $this->size = $size; + + if (!\is_int($error) || !\in_array($error, array( + \UPLOAD_ERR_OK, + \UPLOAD_ERR_INI_SIZE, + \UPLOAD_ERR_FORM_SIZE, + \UPLOAD_ERR_PARTIAL, + \UPLOAD_ERR_NO_FILE, + \UPLOAD_ERR_NO_TMP_DIR, + \UPLOAD_ERR_CANT_WRITE, + \UPLOAD_ERR_EXTENSION, + ))) { + throw new InvalidArgumentException( + 'Invalid error code, must be an UPLOAD_ERR_* constant' + ); + } + $this->error = $error; + $this->filename = $filename; + $this->mediaType = $mediaType; + } + + /** + * {@inheritdoc} + */ + public function getStream() + { + if ($this->error !== \UPLOAD_ERR_OK) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + return $this->stream; + } + + /** + * {@inheritdoc} + */ + public function moveTo($targetPath) + { + throw new RuntimeException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + */ + public function getClientFilename() + { + return $this->filename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->mediaType; + } +} diff --git a/vendor/react/http/src/Message/Response.php b/vendor/react/http/src/Message/Response.php new file mode 100644 index 0000000..edd6245 --- /dev/null +++ b/vendor/react/http/src/Message/Response.php @@ -0,0 +1,291 @@ +<?php + +namespace React\Http\Message; + +use Fig\Http\Message\StatusCodeInterface; +use Psr\Http\Message\StreamInterface; +use React\Http\Io\BufferedBody; +use React\Http\Io\HttpBodyStream; +use React\Stream\ReadableStreamInterface; +use RingCentral\Psr7\Response as Psr7Response; + +/** + * Represents an outgoing server response message. + * + * ```php + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * array( + * 'Content-Type' => 'text/html' + * ), + * "<html>Hello world!</html>\n" + * ); + * ``` + * + * This class implements the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + * which in turn extends the + * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + * + * On top of this, this class implements the + * [PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) + * which means that most common HTTP status codes are available as class + * constants with the `STATUS_*` prefix. For instance, the `200 OK` and + * `404 Not Found` status codes can used as `Response::STATUS_OK` and + * `Response::STATUS_NOT_FOUND` respectively. + * + * > Internally, this implementation builds on top of an existing incoming + * response message and only adds required streaming support. This base class is + * considered an implementation detail that may change in the future. + * + * @see \Psr\Http\Message\ResponseInterface + */ +final class Response extends Psr7Response implements StatusCodeInterface +{ + /** + * Create an HTML response + * + * ```php + * $html = <<<HTML + * <!doctype html> + * <html> + * <body>Hello wörld!</body> + * </html> + * + * HTML; + * + * $response = React\Http\Message\Response::html($html); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/html; charset=utf-8' + * ], + * $html + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given HTTP source + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::html( + * "<h1>Error</h1>\n<p>Invalid user name given.</p>\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $html + * @return self + */ + public static function html($html) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html); + } + + /** + * Create a JSON response + * + * ```php + * $response = React\Http\Message\Response::json(['name' => 'Alice']); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode( + * ['name' => 'Alice'], + * JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + * ) . "\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given structured + * data encoded as a JSON text. + * + * The given structured data will be encoded as a JSON text. Any `string` + * values in the data must be encoded in UTF-8 (Unicode). If the encoding + * fails, this method will throw an `InvalidArgumentException`. + * + * By default, the given structured data will be encoded with the flags as + * shown above. This includes pretty printing (PHP 5.4+) and preserving + * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is + * assumed any additional data overhead is usually compensated by using HTTP + * response compression. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::json( + * ['error' => 'Invalid user name given'] + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param mixed $data + * @return self + * @throws \InvalidArgumentException when encoding fails + */ + public static function json($data) + { + $json = @\json_encode( + $data, + (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) + ); + + // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5 + if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) { + throw new \InvalidArgumentException( + 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), + \json_last_error() + ); + } + + return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n"); + } + + /** + * Create a plaintext response + * + * ```php + * $response = React\Http\Message\Response::plaintext("Hello wörld!\n"); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/plain; charset=utf-8' + * ], + * "Hello wörld!\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given plaintext + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::plaintext( + * "Error: Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $text + * @return self + */ + public static function plaintext($text) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text); + } + + /** + * Create an XML response + * + * ```php + * $xml = <<<XML + * <?xml version="1.0" encoding="utf-8"?> + * <body> + * <greeting>Hello wörld!</greeting> + * </body> + * + * XML; + * + * $response = React\Http\Message\Response::xml($xml); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/xml' + * ], + * $xml + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given XML source + * string. It's generally recommended to use UTF-8 (Unicode) and specify + * this as part of the leading XML declaration and to end the given XML + * source string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::xml( + * "<error><message>Invalid user name given.</message></error>\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $xml + * @return self + */ + public static function xml($xml) + { + return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); + } + + /** + * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants + * @param array<string,string|string[]> $headers additional response headers + * @param string|ReadableStreamInterface|StreamInterface $body response body + * @param string $version HTTP protocol version (e.g. 1.1/1.0) + * @param ?string $reason custom HTTP response phrase + * @throws \InvalidArgumentException for an invalid body + */ + public function __construct( + $status = self::STATUS_OK, + array $headers = array(), + $body = '', + $version = '1.1', + $reason = null + ) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $body = new HttpBodyStream($body, null); + } elseif (!$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid response body given'); + } + + parent::__construct( + $status, + $headers, + $body, + $version, + $reason + ); + } +} diff --git a/vendor/react/http/src/Message/ResponseException.php b/vendor/react/http/src/Message/ResponseException.php new file mode 100644 index 0000000..f4912c9 --- /dev/null +++ b/vendor/react/http/src/Message/ResponseException.php @@ -0,0 +1,43 @@ +<?php + +namespace React\Http\Message; + +use RuntimeException; +use Psr\Http\Message\ResponseInterface; + +/** + * The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject + * a request promise if the remote server returns a non-success status code + * (anything but 2xx or 3xx). + * You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + * + * The `getCode(): int` method can be used to + * return the HTTP response status code. + */ +final class ResponseException extends RuntimeException +{ + private $response; + + public function __construct(ResponseInterface $response, $message = null, $code = null, $previous = null) + { + if ($message === null) { + $message = 'HTTP status code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ')'; + } + if ($code === null) { + $code = $response->getStatusCode(); + } + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + /** + * Access its underlying response object. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/vendor/react/http/src/Message/ServerRequest.php b/vendor/react/http/src/Message/ServerRequest.php new file mode 100644 index 0000000..f446f24 --- /dev/null +++ b/vendor/react/http/src/Message/ServerRequest.php @@ -0,0 +1,197 @@ +<?php + +namespace React\Http\Message; + +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; +use React\Http\Io\BufferedBody; +use React\Http\Io\HttpBodyStream; +use React\Stream\ReadableStreamInterface; +use RingCentral\Psr7\Request; + +/** + * Respresents an incoming server request message. + * + * This class implements the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) + * which extends the + * [PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) + * which in turn extends the + * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + * + * This is mostly used internally to represent each incoming request message. + * Likewise, you can also use this class in test cases to test how your web + * application reacts to certain HTTP requests. + * + * > Internally, this implementation builds on top of an existing outgoing + * request message and only adds required server methods. This base class is + * considered an implementation detail that may change in the future. + * + * @see ServerRequestInterface + */ +final class ServerRequest extends Request implements ServerRequestInterface +{ + private $attributes = array(); + + private $serverParams; + private $fileParams = array(); + private $cookies = array(); + private $queryParams = array(); + private $parsedBody; + + /** + * @param string $method HTTP method for the request. + * @param string|UriInterface $url URL for the request. + * @param array<string,string|string[]> $headers Headers for the message. + * @param string|ReadableStreamInterface|StreamInterface $body Message body. + * @param string $version HTTP protocol version. + * @param array<string,string> $serverParams server-side parameters + * @throws \InvalidArgumentException for an invalid URL or body + */ + public function __construct( + $method, + $url, + array $headers = array(), + $body = '', + $version = '1.1', + $serverParams = array() + ) { + $stream = null; + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $stream = $body; + $body = null; + } elseif (!$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid server request body given'); + } + + $this->serverParams = $serverParams; + parent::__construct($method, $url, $headers, $body, $version); + + if ($stream !== null) { + $size = (int) $this->getHeaderLine('Content-Length'); + if (\strtolower($this->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $size = null; + } + $this->stream = new HttpBodyStream($stream, $size); + } + + $query = $this->getUri()->getQuery(); + if ($query !== '') { + \parse_str($query, $this->queryParams); + } + + // Multiple cookie headers are not allowed according + // to https://tools.ietf.org/html/rfc6265#section-5.4 + $cookieHeaders = $this->getHeader("Cookie"); + + if (count($cookieHeaders) === 1) { + $this->cookies = $this->parseCookie($cookieHeaders[0]); + } + } + + public function getServerParams() + { + return $this->serverParams; + } + + public function getCookieParams() + { + return $this->cookies; + } + + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = $cookies; + return $new; + } + + public function getQueryParams() + { + return $this->queryParams; + } + + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + return $new; + } + + public function getUploadedFiles() + { + return $this->fileParams; + } + + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->fileParams = $uploadedFiles; + return $new; + } + + public function getParsedBody() + { + return $this->parsedBody; + } + + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + return $new; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getAttribute($name, $default = null) + { + if (!\array_key_exists($name, $this->attributes)) { + return $default; + } + return $this->attributes[$name]; + } + + public function withAttribute($name, $value) + { + $new = clone $this; + $new->attributes[$name] = $value; + return $new; + } + + public function withoutAttribute($name) + { + $new = clone $this; + unset($new->attributes[$name]); + return $new; + } + + /** + * @param string $cookie + * @return array + */ + private function parseCookie($cookie) + { + $cookieArray = \explode(';', $cookie); + $result = array(); + + foreach ($cookieArray as $pair) { + $pair = \trim($pair); + $nameValuePair = \explode('=', $pair, 2); + + if (\count($nameValuePair) === 2) { + $key = \urldecode($nameValuePair[0]); + $value = \urldecode($nameValuePair[1]); + $result[$key] = $value; + } + } + + return $result; + } +} diff --git a/vendor/react/http/src/Middleware/LimitConcurrentRequestsMiddleware.php b/vendor/react/http/src/Middleware/LimitConcurrentRequestsMiddleware.php new file mode 100644 index 0000000..5333810 --- /dev/null +++ b/vendor/react/http/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -0,0 +1,211 @@ +<?php + +namespace React\Http\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\HttpBodyStream; +use React\Http\Io\PauseBufferStream; +use React\Promise; +use React\Promise\PromiseInterface; +use React\Promise\Deferred; +use React\Stream\ReadableStreamInterface; + +/** + * Limits how many next handlers can be executed concurrently. + * + * If this middleware is invoked, it will check if the number of pending + * handlers is below the allowed limit and then simply invoke the next handler + * and it will return whatever the next handler returns (or throws). + * + * If the number of pending handlers exceeds the allowed limit, the request will + * be queued (and its streaming body will be paused) and it will return a pending + * promise. + * Once a pending handler returns (or throws), it will pick the oldest request + * from this queue and invokes the next handler (and its streaming body will be + * resumed). + * + * The following example shows how this middleware can be used to ensure no more + * than 10 handlers will be invoked at once: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), + * $handler + * ); + * ``` + * + * Similarly, this middleware is often used in combination with the + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to limit the total number of requests that can be buffered at once: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * ); + * ``` + * + * More sophisticated examples include limiting the total number of requests + * that can be buffered at once and then ensure the actual request handler only + * processes one request after another without any concurrency: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) + * $handler + * ); + * ``` + * + * @see RequestBodyBufferMiddleware + */ +final class LimitConcurrentRequestsMiddleware +{ + private $limit; + private $pending = 0; + private $queue = array(); + + /** + * @param int $limit Maximum amount of concurrent requests handled. + * + * For example when $limit is set to 10, 10 requests will flow to $next + * while more incoming requests have to wait until one is done. + */ + public function __construct($limit) + { + $this->limit = $limit; + } + + public function __invoke(ServerRequestInterface $request, $next) + { + // happy path: simply invoke next request handler if we're below limit + if ($this->pending < $this->limit) { + ++$this->pending; + + try { + $response = $next($request); + } catch (\Exception $e) { + $this->processQueue(); + throw $e; + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // handle Errors just like Exceptions (PHP 7+ only) + $this->processQueue(); + throw $e; // @codeCoverageIgnoreEnd + } + + // happy path: if next request handler returned immediately, + // we can simply try to invoke the next queued request + if ($response instanceof ResponseInterface) { + $this->processQueue(); + return $response; + } + + // if the next handler returns a pending promise, we have to + // await its resolution before invoking next queued request + return $this->await(Promise\resolve($response)); + } + + // if we reach this point, then this request will need to be queued + // check if the body is streaming, in which case we need to buffer everything + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface) { + // pause actual body to stop emitting data until the handler is called + $size = $body->getSize(); + $body = new PauseBufferStream($body); + $body->pauseImplicit(); + + // replace with buffering body to ensure any readable events will be buffered + $request = $request->withBody(new HttpBodyStream( + $body, + $size + )); + } + + // get next queue position + $queue =& $this->queue; + $queue[] = null; + \end($queue); + $id = \key($queue); + + $deferred = new Deferred(function ($_, $reject) use (&$queue, $id) { + // queued promise cancelled before its next handler is invoked + // remove from queue and reject explicitly + unset($queue[$id]); + $reject(new \RuntimeException('Cancelled queued next handler')); + }); + + // queue request and process queue if pending does not exceed limit + $queue[$id] = $deferred; + + $pending = &$this->pending; + $that = $this; + return $deferred->promise()->then(function () use ($request, $next, $body, &$pending, $that) { + // invoke next request handler + ++$pending; + + try { + $response = $next($request); + } catch (\Exception $e) { + $that->processQueue(); + throw $e; + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // handle Errors just like Exceptions (PHP 7+ only) + $that->processQueue(); + throw $e; // @codeCoverageIgnoreEnd + } + + // resume readable stream and replay buffered events + if ($body instanceof PauseBufferStream) { + $body->resumeImplicit(); + } + + // if the next handler returns a pending promise, we have to + // await its resolution before invoking next queued request + return $that->await(Promise\resolve($response)); + }); + } + + /** + * @internal + * @param PromiseInterface $promise + * @return PromiseInterface + */ + public function await(PromiseInterface $promise) + { + $that = $this; + + return $promise->then(function ($response) use ($that) { + $that->processQueue(); + + return $response; + }, function ($error) use ($that) { + $that->processQueue(); + + return Promise\reject($error); + }); + } + + /** + * @internal + */ + public function processQueue() + { + // skip if we're still above concurrency limit or there's no queued request waiting + if (--$this->pending >= $this->limit || !$this->queue) { + return; + } + + $first = \reset($this->queue); + unset($this->queue[key($this->queue)]); + + $first->resolve(); + } +} diff --git a/vendor/react/http/src/Middleware/RequestBodyBufferMiddleware.php b/vendor/react/http/src/Middleware/RequestBodyBufferMiddleware.php new file mode 100644 index 0000000..c13a5de --- /dev/null +++ b/vendor/react/http/src/Middleware/RequestBodyBufferMiddleware.php @@ -0,0 +1,70 @@ +<?php + +namespace React\Http\Middleware; + +use OverflowException; +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\BufferedBody; +use React\Http\Io\IniUtil; +use React\Promise\Stream; +use React\Stream\ReadableStreamInterface; + +final class RequestBodyBufferMiddleware +{ + private $sizeLimit; + + /** + * @param int|string|null $sizeLimit Either an int with the max request body size + * in bytes or an ini like size string + * or null to use post_max_size from PHP's + * configuration. (Note that the value from + * the CLI configuration will be used.) + */ + public function __construct($sizeLimit = null) + { + if ($sizeLimit === null) { + $sizeLimit = \ini_get('post_max_size'); + } + + $this->sizeLimit = IniUtil::iniSizeToBytes($sizeLimit); + } + + public function __invoke(ServerRequestInterface $request, $stack) + { + $body = $request->getBody(); + $size = $body->getSize(); + + // happy path: skip if body is known to be empty (or is already buffered) + if ($size === 0 || !$body instanceof ReadableStreamInterface) { + // replace with empty body if body is streaming (or buffered size exceeds limit) + if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) { + $request = $request->withBody(new BufferedBody('')); + } + + return $stack($request); + } + + // request body of known size exceeding limit + $sizeLimit = $this->sizeLimit; + if ($size > $this->sizeLimit) { + $sizeLimit = 0; + } + + return Stream\buffer($body, $sizeLimit)->then(function ($buffer) use ($request, $stack) { + $request = $request->withBody(new BufferedBody($buffer)); + + return $stack($request); + }, function ($error) use ($stack, $request, $body) { + // On buffer overflow keep the request body stream in, + // but ignore the contents and wait for the close event + // before passing the request on to the next middleware. + if ($error instanceof OverflowException) { + return Stream\first($body, 'close')->then(function () use ($stack, $request) { + return $stack($request); + }); + } + + throw $error; + }); + } +} diff --git a/vendor/react/http/src/Middleware/RequestBodyParserMiddleware.php b/vendor/react/http/src/Middleware/RequestBodyParserMiddleware.php new file mode 100644 index 0000000..be5ba16 --- /dev/null +++ b/vendor/react/http/src/Middleware/RequestBodyParserMiddleware.php @@ -0,0 +1,46 @@ +<?php + +namespace React\Http\Middleware; + +use Psr\Http\Message\ServerRequestInterface; +use React\Http\Io\MultipartParser; + +final class RequestBodyParserMiddleware +{ + private $multipart; + + /** + * @param int|string|null $uploadMaxFilesize + * @param int|null $maxFileUploads + */ + public function __construct($uploadMaxFilesize = null, $maxFileUploads = null) + { + $this->multipart = new MultipartParser($uploadMaxFilesize, $maxFileUploads); + } + + public function __invoke(ServerRequestInterface $request, $next) + { + $type = \strtolower($request->getHeaderLine('Content-Type')); + list ($type) = \explode(';', $type); + + if ($type === 'application/x-www-form-urlencoded') { + return $next($this->parseFormUrlencoded($request)); + } + + if ($type === 'multipart/form-data') { + return $next($this->multipart->parse($request)); + } + + return $next($request); + } + + private function parseFormUrlencoded(ServerRequestInterface $request) + { + // parse string into array structure + // ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level) + $ret = array(); + @\parse_str((string)$request->getBody(), $ret); + + return $request->withParsedBody($ret); + } +} diff --git a/vendor/react/http/src/Middleware/StreamingRequestMiddleware.php b/vendor/react/http/src/Middleware/StreamingRequestMiddleware.php new file mode 100644 index 0000000..6ab74b7 --- /dev/null +++ b/vendor/react/http/src/Middleware/StreamingRequestMiddleware.php @@ -0,0 +1,69 @@ +<?php + +namespace React\Http\Middleware; + +use Psr\Http\Message\ServerRequestInterface; + +/** + * Process incoming requests with a streaming request body (without buffering). + * + * This allows you to process requests of any size without buffering the request + * body in memory. Instead, it will represent the request body as a + * [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + * that emit chunks of incoming data as it is received: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * function (Psr\Http\Message\ServerRequestInterface $request) { + * $body = $request->getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * return new React\Promise\Promise(function ($resolve) use ($body) { + * $bytes = 0; + * $body->on('data', function ($chunk) use (&$bytes) { + * $bytes += \count($chunk); + * }); + * $body->on('close', function () use (&$bytes, $resolve) { + * $resolve(new React\Http\Response( + * 200, + * [], + * "Received $bytes bytes\n" + * )); + * }); + * }); + * } + * ); + * ``` + * + * See also [streaming incoming request](../../README.md#streaming-incoming-request) + * for more details. + * + * Additionally, this middleware can be used in combination with the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * ); + * ``` + * + * > Internally, this class is used as a "marker" to not trigger the default + * request buffering behavior in the `HttpServer`. It does not implement any logic + * on its own. + */ +final class StreamingRequestMiddleware +{ + public function __invoke(ServerRequestInterface $request, $next) + { + return $next($request); + } +} diff --git a/vendor/react/http/src/Server.php b/vendor/react/http/src/Server.php new file mode 100644 index 0000000..9bb9cf7 --- /dev/null +++ b/vendor/react/http/src/Server.php @@ -0,0 +1,18 @@ +<?php + +namespace React\Http; + +// Deprecated `Server` is an alias for new `HttpServer` to ensure existing code continues to work as-is. +\class_alias(__NAMESPACE__ . '\\HttpServer', __NAMESPACE__ . '\\Server', true); + +// Aid static analysis and IDE autocompletion about this deprecation, +// but don't actually execute during runtime because `HttpServer` is final. +if (!\class_exists(__NAMESPACE__ . '\\Server', false)) { + /** + * @deprecated 1.5.0 See HttpServer instead + * @see HttpServer + */ + final class Server extends HttpServer + { + } +} |