This commit is contained in:
2024-11-27 21:34:07 +02:00
parent 638bcba894
commit b6d1215999
190 changed files with 31518 additions and 0 deletions

920
vendor/react/http/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,920 @@
# Changelog
## 1.11.0 (2024-11-20)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable types.
(#537 by @clue)
* Feature: Allow underscore character in Uri host.
(#524 by @lulhum)
* Improve test suite to fix expected error code when ext-sockets is not enabled.
(#539 by @WyriHaximus)
## 1.10.0 (2024-03-27)
* Feature: Add new PSR-7 implementation and remove dated RingCentral PSR-7 dependency.
(#518, #519, #520 and #522 by @clue)
This changeset allows us to maintain our own PSR-7 implementation and reduce
dependencies on external projects. It also improves performance slightly and
does not otherwise affect our public API. If you want to explicitly install
the old RingCentral PSR-7 dependency, you can still install it like this:
```bash
composer require ringcentral/psr7
```
* Feature: Add new `Uri` class for new PSR-7 implementation.
(#521 by @clue)
* Feature: Validate outgoing HTTP message headers and reject invalid messages.
(#523 by @clue)
* Feature: Full PHP 8.3 compatibility.
(#508 by @clue)
* Fix: Fix HTTP client to omit `Transfer-Encoding: chunked` when streaming empty request body.
(#516 by @clue)
* Fix: Ensure connection close handler is cleaned up for each request.
(#515 by @WyriHaximus)
* Update test suite and avoid unhandled promise rejections.
(#501 and #502 by @clue)
## 1.9.0 (2023-04-26)
This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component.
* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component
that affects all versions between `v0.8.0` and `v1.8.0`. All users are encouraged to upgrade immediately.
(CVE-2023-26044 reported and fixed by @WyriHaximus)
* Feature: Support HTTP keep-alive for HTTP client (reusing persistent connections).
(#481, #484, #486 and #495 by @clue)
This feature offers significant performance improvements when sending many
requests to the same host as it avoids recreating the underlying TCP/IP
connection and repeating the TLS handshake for secure HTTPS requests.
```php
$browser = new React\Http\Browser();
// Up to 300% faster! HTTP keep-alive is enabled by default
$response = React\Async\await($browser->get('https://httpbingo.org/redirect/6'));
assert($response instanceof Psr\Http\Message\ResponseInterface);
```
* Feature: Add `Request` class to represent outgoing HTTP request message.
(#480 by @clue)
* Feature: Preserve request method and body for `307 Temporary Redirect` and `308 Permanent Redirect`.
(#442 by @dinooo13)
* Feature: Include buffer logic to avoid dependency on reactphp/promise-stream.
(#482 by @clue)
* Improve test suite and project setup and report failed assertions.
(#478 by @clue, #487 and #491 by @WyriHaximus and #475 and #479 by @SimonFrings)
## 1.8.0 (2022-09-29)
* Feature: Support for default request headers.
(#461 by @51imyy)
```php
$browser = new React\Http\Browser();
$browser = $browser->withHeader('User-Agent', 'ACME');
$browser->get($url)->then(…);
```
* Feature: Forward compatibility with upcoming Promise v3.
(#460 by @clue)
## 1.7.0 (2022-08-23)
This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component.
* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component
that affects all versions between `v0.7.0` and `v1.6.0`. All users are encouraged to upgrade immediately.
Special thanks to Marco Squarcina (TU Wien) for reporting this and working with us to coordinate this release.
(CVE-2022-36032 reported by @lavish and fixed by @clue)
* Feature: Improve HTTP server performance by ~20%, reuse syscall values for clock time and socket addresses.
(#457 and #467 by @clue)
* Feature: Full PHP 8.2+ compatibility, refactor internal `Transaction` to avoid assigning dynamic properties.
(#459 by @clue and #466 by @WyriHaximus)
* Feature / Fix: Allow explicit `Content-Length` response header on `HEAD` requests.
(#444 by @mrsimonbennett)
* Minor documentation improvements.
(#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings)
* Improve test suite, update to use new reactphp/async package instead of clue/reactphp-block,
skip memory tests when lowering memory limit fails and fix legacy HHVM build.
(#464 and #440 by @clue and #450 by @SimonFrings)
## 1.6.0 (2022-02-03)
* Feature: Add factory methods for common HTML/JSON/plaintext/XML response types.
(#439 by @clue)
```php
$response = React\Http\Response\html("<h1>Hello wörld!</h1>\n");
$response = React\Http\Response\json(['message' => 'Hello wörld!']);
$response = React\Http\Response\plaintext("Hello wörld!\n");
$response = React\Http\Response\xml("<message>Hello wörld!</message>\n");
```
* Feature: Expose all status code constants via `Response` class.
(#432 by @clue)
```php
$response = new React\Http\Message\Response(
React\Http\Message\Response::STATUS_OK, // 200 OK
);
$response = new React\Http\Message\Response(
React\Http\Message\Response::STATUS_NOT_FOUND, // 404 Not Found
);
```
* Feature: Full support for PHP 8.1 release.
(#433 by @SimonFrings and #434 by @clue)
* Feature / Fix: Improve protocol handling for HTTP responses with no body.
(#429 and #430 by @clue)
* Internal refactoring and internal improvements for handling requests and responses.
(#422 by @WyriHaximus and #431 by @clue)
* Improve documentation, update proxy examples, include error reporting in examples.
(#420, #424, #426, and #427 by @clue)
* Update test suite to use default loop.
(#438 by @clue)
## 1.5.0 (2021-08-04)
* Feature: Update `Browser` signature to take optional `$connector` as first argument and
to match new Socket API without nullable loop arguments.
(#418 and #419 by @clue)
```php
// unchanged
$browser = new React\Http\Browser();
// deprecated
$browser = new React\Http\Browser(null, $connector);
$browser = new React\Http\Browser($loop, $connector);
// new
$browser = new React\Http\Browser($connector);
$browser = new React\Http\Browser($connector, $loop);
```
* Feature: Rename `Server` to `HttpServer` to avoid class name collisions and
to avoid any ambiguities with regards to the new `SocketServer` API.
(#417 and #419 by @clue)
```php
// deprecated
$server = new React\Http\Server($handler);
$server->listen(new React\Socket\Server(8080));
// new
$http = new React\Http\HttpServer($handler);
$http->listen(new React\Socket\SocketServer('127.0.0.1:8080'));
```
## 1.4.0 (2021-07-11)
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
(#410 by @clue)
```php
// old (still supported)
$browser = new React\Http\Browser($loop);
$server = new React\Http\Server($loop, $handler);
// new (using default loop)
$browser = new React\Http\Browser();
$server = new React\Http\Server($handler);
```
## 1.3.0 (2021-04-11)
* Feature: Support persistent connections (`Connection: keep-alive`).
(#405 by @clue)
This shows a noticeable performance improvement especially when benchmarking
using persistent connections (which is the default pretty much everywhere).
Together with other changes in this release, this improves benchmarking
performance by around 100%.
* Feature: Require `Host` request header for HTTP/1.1 requests.
(#404 by @clue)
* Minor documentation improvements.
(#398 by @fritz-gerneth and #399 and #400 by @pavog)
* Improve test suite, use GitHub actions for continuous integration (CI).
(#402 by @SimonFrings)
## 1.2.0 (2020-12-04)
* Feature: Keep request body in memory also after consuming request body.
(#395 by @clue)
This means consumers can now always access the complete request body as
detailed in the documentation. This allows building custom parsers and more
advanced processing models without having to mess with the default parsers.
## 1.1.0 (2020-09-11)
* Feature: Support upcoming PHP 8 release, update to reactphp/socket v1.6 and adjust type checks for invalid chunk headers.
(#391 by @clue)
* Feature: Consistently resolve base URL according to HTTP specs.
(#379 by @clue)
* Feature / Fix: Expose `Transfer-Encoding: chunked` response header and fix chunked responses for `HEAD` requests.
(#381 by @clue)
* Internal refactoring to remove unneeded `MessageFactory` and `Response` classes.
(#380 and #389 by @clue)
* Minor documentation improvements and improve test suite, update to support PHPUnit 9.3.
(#385 by @clue and #393 by @SimonFrings)
## 1.0.0 (2020-07-11)
A major new feature release, see [**release announcement**](https://clue.engineering/2020/announcing-reactphp-http).
* First stable LTS release, now following [SemVer](https://semver.org/).
We'd like to emphasize that this component is production ready and battle-tested.
We plan to support all long-term support (LTS) releases for at least 24 months,
so you have a rock-solid foundation to build on top of.
This update involves some major new features and a number of BC breaks due to
some necessary API cleanup. We've tried hard to avoid BC breaks where possible
and minimize impact otherwise. We expect that most consumers of this package
will be affected by BC breaks, but updating should take no longer than a few
minutes. See below for more details:
* Feature: Add async HTTP client implementation.
(#368 by @clue)
```php
$browser = new React\Http\Browser($loop);
$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
echo $response->getBody();
});
```
The code has been imported as-is from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz),
with only minor changes to the namespace and we otherwise leave all the existing APIs unchanged.
Upgrading from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz)
to this release should be a matter of updating some namespace references only:
```php
// old
$browser = new Clue\React\Buzz\Browser($loop);
// new
$browser = new React\Http\Browser($loop);
```
* Feature / BC break: Add `LoopInterface` as required first constructor argument to `Server` and
change `Server` to accept variadic middleware handlers instead of `array`.
(#361 and #362 by @WyriHaximus)
```php
// old
$server = new React\Http\Server($handler);
$server = new React\Http\Server([$middleware, $handler]);
// new
$server = new React\Http\Server($loop, $handler);
$server = new React\Http\Server($loop, $middleware, $handler);
```
* Feature / BC break: Move `Response` class to `React\Http\Message\Response` and
expose `ServerRequest` class to `React\Http\Message\ServerRequest`.
(#370 by @clue)
```php
// old
$response = new React\Http\Response(200, [], 'Hello!');
// new
$response = new React\Http\Message\Response(200, [], 'Hello!');
```
* Feature / BC break: Add `StreamingRequestMiddleware` to stream incoming requests, mark `StreamingServer` as internal.
(#367 by @clue)
```php
// old: advanced StreamingServer is now internal only
$server = new React\Http\StreamingServer($handler);
// new: use StreamingRequestMiddleware instead of StreamingServer
$server = new React\Http\Server(
$loop,
new React\Http\Middleware\StreamingRequestMiddleware(),
$handler
);
```
* Feature / BC break: Improve default concurrency to 1024 requests and cap default request buffer at 64K.
(#371 by @clue)
This improves default concurrency to 1024 requests and caps the default request buffer at 64K.
The previous defaults resulted in just 4 concurrent requests with a request buffer of 8M.
See [`Server`](README.md#server) for details on how to override these defaults.
* Feature: Expose ReactPHP in `User-Agent` client-side request header and in `Server` server-side response header.
(#374 by @clue)
* Mark all classes as `final` to discourage inheriting from it.
(#373 by @WyriHaximus)
* Improve documentation and use fully-qualified class names throughout the documentation and
add ReactPHP core team as authors to `composer.json` and license file.
(#366 and #369 by @WyriHaximus and #375 by @clue)
* Improve test suite and support skipping all online tests with `--exclude-group internet`.
(#372 by @clue)
## 0.8.7 (2020-07-05)
* Fix: Fix parsing multipart request body with quoted header parameters (dot net).
(#363 by @ebimmel)
* Fix: Fix calculating concurrency when `post_max_size` ini is unlimited.
(#365 by @clue)
* Improve test suite to run tests on PHPUnit 9 and clean up test suite.
(#364 by @SimonFrings)
## 0.8.6 (2020-01-12)
* Fix: Fix parsing `Cookie` request header with comma in its values.
(#352 by @fiskie)
* Fix: Avoid unneeded warning when decoding invalid data on PHP 7.4.
(#357 by @WyriHaximus)
* Add .gitattributes to exclude dev files from exports.
(#353 by @reedy)
## 0.8.5 (2019-10-29)
* Internal refactorings and optimizations to improve request parsing performance.
Benchmarks suggest number of requests/s improved by ~30% for common `GET` requests.
(#345, #346, #349 and #350 by @clue)
* Add documentation and example for JSON/XML request body and
improve documentation for concurrency and streaming requests and for error handling.
(#341 and #342 by @clue)
## 0.8.4 (2019-01-16)
* Improvement: Internal refactoring to simplify response header logic.
(#321 by @clue)
* Improvement: Assign Content-Length response header automatically only when size is known.
(#329 by @clue)
* Improvement: Import global functions for better performance.
(#330 by @WyriHaximus)
## 0.8.3 (2018-04-11)
* Feature: Do not pause connection stream to detect closed connections immediately.
(#315 by @clue)
* Feature: Keep incoming `Transfer-Encoding: chunked` request header.
(#316 by @clue)
* Feature: Reject invalid requests that contain both `Content-Length` and `Transfer-Encoding` request headers.
(#318 by @clue)
* Minor internal refactoring to simplify connection close logic after sending response.
(#317 by @clue)
## 0.8.2 (2018-04-06)
* Fix: Do not pass `$next` handler to final request handler.
(#308 by @clue)
* Fix: Fix awaiting queued handlers when cancelling a queued handler.
(#313 by @clue)
* Fix: Fix Server to skip `SERVER_ADDR` params for Unix domain sockets (UDS).
(#307 by @clue)
* Documentation for PSR-15 middleware and minor documentation improvements.
(#314 by @clue and #297, #298 and #310 by @seregazhuk)
* Minor code improvements and micro optimizations.
(#301 by @seregazhuk and #305 by @kalessil)
## 0.8.1 (2018-01-05)
* Major request handler performance improvement. Benchmarks suggest number of
requests/s improved by more than 50% for common `GET` requests!
We now avoid queuing, buffering and wrapping incoming requests in promises
when we're below limits and instead can directly process common requests.
(#291, #292, #293, #294 and #296 by @clue)
* Fix: Fix concurrent invoking next middleware request handlers
(#293 by @clue)
* Small code improvements
(#286 by @seregazhuk)
* Improve test suite to be less fragile when using `ext-event` and
fix test suite forward compatibility with upcoming EventLoop releases
(#288 and #290 by @clue)
## 0.8.0 (2017-12-12)
* Feature / BC break: Add new `Server` facade that buffers and parses incoming
HTTP requests. This provides full PSR-7 compatibility, including support for
form submissions with POST fields and file uploads.
The old `Server` has been renamed to `StreamingServer` for advanced usage
and is used internally.
(#266, #271, #281, #282, #283 and #284 by @WyriHaximus and @clue)
```php
// old: handle incomplete/streaming requests
$server = new Server($handler);
// new: handle complete, buffered and parsed requests
// new: full PSR-7 support, including POST fields and file uploads
$server = new Server($handler);
// new: handle incomplete/streaming requests
$server = new StreamingServer($handler);
```
> While this is technically a small BC break, this should in fact not break
most consuming code. If you rely on the old request streaming, you can
explicitly use the advanced `StreamingServer` to restore old behavior.
* Feature: Add support for middleware request handler arrays
(#215, #228, #229, #236, #237, #238, #246, #247, #277, #279 and #285 by @WyriHaximus, @clue and @jsor)
```php
// new: middleware request handler arrays
$server = new Server(array(
function (ServerRequestInterface $request, callable $next) {
$request = $request->withHeader('Processed', time());
return $next($request);
},
function (ServerRequestInterface $request) {
return new Response();
}
));
```
* Feature: Add support for limiting how many next request handlers can be
executed concurrently (`LimitConcurrentRequestsMiddleware`)
(#272 by @clue and @WyriHaximus)
```php
// new: explicitly limit concurrency
$server = new Server(array(
new LimitConcurrentRequestsMiddleware(10),
$handler
));
```
* Feature: Add support for buffering the incoming request body
(`RequestBodyBufferMiddleware`).
This feature mimics PHP's default behavior and respects its `post_max_size`
ini setting by default and allows explicit configuration.
(#216, #224, #263, #276 and #278 by @WyriHaximus and #235 by @andig)
```php
// new: buffer up to 10 requests with 8 MiB each
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(10),
new RequestBodyBufferMiddleware('8M'),
$handler
));
```
* Feature: Add support for parsing form submissions with POST fields and file
uploads (`RequestBodyParserMiddleware`).
This feature mimics PHP's default behavior and respects its ini settings and
`MAX_FILE_SIZE` POST fields by default and allows explicit configuration.
(#220, #226, #252, #261, #264, #265, #267, #268, #274 by @WyriHaximus and @clue)
```php
// new: buffer up to 10 requests with 8 MiB each
// and limit to 4 uploads with 2 MiB each
$server = new StreamingServer(array(
new LimitConcurrentRequestsMiddleware(10),
new RequestBodyBufferMiddleware('8M'),
new RequestBodyParserMiddleware('2M', 4)
$handler
));
```
* Feature: Update Socket to work around sending secure HTTPS responses with PHP < 7.1.4
(#244 by @clue)
* Feature: Support sending same response header multiple times (e.g. `Set-Cookie`)
(#248 by @clue)
* Feature: Raise maximum request header size to 8k to match common implementations
(#253 by @clue)
* Improve test suite by adding forward compatibility with PHPUnit 6, test
against PHP 7.1 and PHP 7.2 and refactor and remove risky and duplicate tests.
(#243, #269 and #270 by @carusogabriel and #249 by @clue)
* Minor code refactoring to move internal classes to `React\Http\Io` namespace
and clean up minor code and documentation issues
(#251 by @clue, #227 by @kalessil, #240 by @christoph-kluge, #230 by @jsor and #280 by @andig)
## 0.7.4 (2017-08-16)
* Improvement: Target evenement 3.0 a long side 2.0 and 1.0
(#212 by @WyriHaximus)
## 0.7.3 (2017-08-14)
* Feature: Support `Throwable` when setting previous exception from server callback
(#155 by @jsor)
* Fix: Fixed URI parsing for origin-form requests that contain scheme separator
such as `/path?param=http://example.com`.
(#209 by @aaronbonneau)
* Improve test suite by locking Travis distro so new defaults will not break the build
(#211 by @clue)
## 0.7.2 (2017-07-04)
* Fix: Stricter check for invalid request-line in HTTP requests
(#206 by @clue)
* Refactor to use HTTP response reason phrases from response object
(#205 by @clue)
## 0.7.1 (2017-06-17)
* Fix: Fix parsing CONNECT request without `Host` header
(#201 by @clue)
* Internal preparation for future PSR-7 `UploadedFileInterface`
(#199 by @WyriHaximus)
## 0.7.0 (2017-05-29)
* Feature / BC break: Use PSR-7 (http-message) standard and
`Request-In-Response-Out`-style request handler callback.
Pass standard PSR-7 `ServerRequestInterface` and expect any standard
PSR-7 `ResponseInterface` in return for the request handler callback.
(#146 and #152 and #170 by @legionth)
```php
// old
$app = function (Request $request, Response $response) {
$response->writeHead(200, array('Content-Type' => 'text/plain'));
$response->end("Hello world!\n");
};
// new
$app = function (ServerRequestInterface $request) {
return new Response(
200,
array('Content-Type' => 'text/plain'),
"Hello world!\n"
);
};
```
A `Content-Length` header will automatically be included if the size can be
determined from the response body.
(#164 by @maciejmrozinski)
The request handler callback will automatically make sure that responses to
HEAD requests and certain status codes, such as `204` (No Content), never
contain a response body.
(#156 by @clue)
The intermediary `100 Continue` response will automatically be sent if
demanded by a HTTP/1.1 client.
(#144 by @legionth)
The request handler callback can now return a standard `Promise` if
processing the request needs some time, such as when querying a database.
Similarly, the request handler may return a streaming response if the
response body comes from a `ReadableStreamInterface` or its size is
unknown in advance.
```php
// old
$app = function (Request $request, Response $response) use ($db) {
$db->query()->then(function ($result) use ($response) {
$response->writeHead(200, array('Content-Type' => 'text/plain'));
$response->end($result);
});
};
// new
$app = function (ServerRequestInterface $request) use ($db) {
return $db->query()->then(function ($result) {
return new Response(
200,
array('Content-Type' => 'text/plain'),
$result
);
});
};
```
Pending promies and response streams will automatically be canceled once the
client connection closes.
(#187 and #188 by @clue)
The `ServerRequestInterface` contains the full effective request URI,
server-side parameters, query parameters and parsed cookies values as
defined in PSR-7.
(#167 by @clue and #174, #175 and #180 by @legionth)
```php
$app = function (ServerRequestInterface $request) {
return new Response(
200,
array('Content-Type' => 'text/plain'),
$request->getUri()->getScheme()
);
};
```
Advanced: Support duplex stream response for `Upgrade` requests such as
`Upgrade: WebSocket` or custom protocols and `CONNECT` requests
(#189 and #190 by @clue)
> Note that the request body will currently not be buffered and parsed by
default, which depending on your particilar use-case, may limit
interoperability with the PSR-7 (http-message) ecosystem.
The provided streaming request body interfaces allow you to perform
buffering and parsing as needed in the request handler callback.
See also the README and examples for more details.
* Feature / BC break: Replace `request` listener with callback function and
use `listen()` method to support multiple listening sockets
(#97 by @legionth and #193 by @clue)
```php
// old
$server = new Server($socket);
$server->on('request', $app);
// new
$server = new Server($app);
$server->listen($socket);
```
* Feature: Support the more advanced HTTP requests, such as
`OPTIONS * HTTP/1.1` (`OPTIONS` method in asterisk-form),
`GET http://example.com/path HTTP/1.1` (plain proxy requests in absolute-form),
`CONNECT example.com:443 HTTP/1.1` (`CONNECT` proxy requests in authority-form)
and sanitize `Host` header value across all requests.
(#157, #158, #161, #165, #169 and #173 by @clue)
* Feature: Forward compatibility with Socket v1.0, v0.8, v0.7 and v0.6 and
forward compatibility with Stream v1.0 and v0.7
(#154, #163, #183, #184 and #191 by @clue)
* Feature: Simplify examples to ease getting started and
add benchmarking example
(#151 and #162 by @clue)
* Improve test suite by adding tests for case insensitive chunked transfer
encoding and ignoring HHVM test failures until Travis tests work again.
(#150 by @legionth and #185 by @clue)
## 0.6.0 (2017-03-09)
* Feature / BC break: The `Request` and `Response` objects now follow strict
stream semantics and their respective methods and events.
(#116, #129, #133, #135, #136, #137, #138, #140, #141 by @legionth
and #122, #123, #130, #131, #132, #142 by @clue)
This implies that the `Server` now supports proper detection of the request
message body stream, such as supporting decoding chunked transfer encoding,
delimiting requests with an explicit `Content-Length` header
and those with an empty request message body.
These streaming semantics are compatible with previous Stream v0.5, future
compatible with v0.5 and upcoming v0.6 versions and can be used like this:
```php
$http->on('request', function (Request $request, Response $response) {
$contentLength = 0;
$request->on('data', function ($data) use (&$contentLength) {
$contentLength += strlen($data);
});
$request->on('end', function () use ($response, &$contentLength){
$response->writeHead(200, array('Content-Type' => 'text/plain'));
$response->end("The length of the submitted request body is: " . $contentLength);
});
// an error occured
// e.g. on invalid chunked encoded data or an unexpected 'end' event
$request->on('error', function (\Exception $exception) use ($response, &$contentLength) {
$response->writeHead(400, array('Content-Type' => 'text/plain'));
$response->end("An error occured while reading at length: " . $contentLength);
});
});
```
Similarly, the `Request` and `Response` now strictly follow the
`close()` method and `close` event semantics.
Closing the `Request` does not interrupt the underlying TCP/IP in
order to allow still sending back a valid response message.
Closing the `Response` does terminate the underlying TCP/IP
connection in order to clean up resources.
You should make sure to always attach a `request` event listener
like above. The `Server` will not respond to an incoming HTTP
request otherwise and keep the TCP/IP connection pending until the
other side chooses to close the connection.
* Feature: Support `HTTP/1.1` and `HTTP/1.0` for `Request` and `Response`.
(#124, #125, #126, #127, #128 by @clue and #139 by @legionth)
The outgoing `Response` will automatically use the same HTTP version as the
incoming `Request` message and will only apply `HTTP/1.1` semantics if
applicable. This includes that the `Response` will automatically attach a
`Date` and `Connection: close` header if applicable.
This implies that the `Server` now automatically responds with HTTP error
messages for invalid requests (status 400) and those exceeding internal
request header limits (status 431).
## 0.5.0 (2017-02-16)
* Feature / BC break: Change `Request` methods to be in line with PSR-7
(#117 by @clue)
* Rename `getQuery()` to `getQueryParams()`
* Rename `getHttpVersion()` to `getProtocolVersion()`
* Change `getHeaders()` to always return an array of string values
for each header
* Feature / BC break: Update Socket component to v0.5 and
add secure HTTPS server support
(#90 and #119 by @clue)
```php
// old plaintext HTTP server
$socket = new React\Socket\Server($loop);
$socket->listen(8080, '127.0.0.1');
$http = new React\Http\Server($socket);
// new plaintext HTTP server
$socket = new React\Socket\Server('127.0.0.1:8080', $loop);
$http = new React\Http\Server($socket);
// new secure HTTPS server
$socket = new React\Socket\Server('127.0.0.1:8080', $loop);
$socket = new React\Socket\SecureServer($socket, $loop, array(
'local_cert' => __DIR__ . '/localhost.pem'
));
$http = new React\Http\Server($socket);
```
* BC break: Mark internal APIs as internal or private and
remove unneeded `ServerInterface`
(#118 by @clue, #95 by @legionth)
## 0.4.4 (2017-02-13)
* Feature: Add request header accessors (à la PSR-7)
(#103 by @clue)
```php
// get value of host header
$host = $request->getHeaderLine('Host');
// get list of all cookie headers
$cookies = $request->getHeader('Cookie');
```
* Feature: Forward `pause()` and `resume()` from `Request` to underlying connection
(#110 by @clue)
```php
// support back-pressure when piping request into slower destination
$request->pipe($dest);
// manually pause/resume request
$request->pause();
$request->resume();
```
* Fix: Fix `100-continue` to be handled case-insensitive and ignore it for HTTP/1.0.
Similarly, outgoing response headers are now handled case-insensitive, e.g
we no longer apply chunked transfer encoding with mixed-case `Content-Length`.
(#107 by @clue)
```php
// now handled case-insensitive
$request->expectsContinue();
// now works just like properly-cased header
$response->writeHead($status, array('content-length' => 0));
```
* Fix: Do not emit empty `data` events and ignore empty writes in order to
not mess up chunked transfer encoding
(#108 and #112 by @clue)
* Lock and test minimum required dependency versions and support PHPUnit v5
(#113, #115 and #114 by @andig)
## 0.4.3 (2017-02-10)
* Fix: Do not take start of body into account when checking maximum header size
(#88 by @nopolabs)
* Fix: Remove `data` listener if `HeaderParser` emits an error
(#83 by @nick4fake)
* First class support for PHP 5.3 through PHP 7 and HHVM
(#101 and #102 by @clue, #66 by @WyriHaximus)
* Improve test suite by adding PHPUnit to require-dev,
improving forward compatibility with newer PHPUnit versions
and replacing unneeded test stubs
(#92 and #93 by @nopolabs, #100 by @legionth)
## 0.4.2 (2016-11-09)
* Remove all listeners after emitting error in RequestHeaderParser #68 @WyriHaximus
* Catch Guzzle parse request errors #65 @WyriHaximus
* Remove branch-alias definition as per reactphp/react#343 #58 @WyriHaximus
* Add functional example to ease getting started #64 by @clue
* Naming, immutable array manipulation #37 @cboden
## 0.4.1 (2015-05-21)
* Replaced guzzle/parser with guzzlehttp/psr7 by @cboden
* FIX Continue Header by @iannsp
* Missing type hint by @marenzo
## 0.4.0 (2014-02-02)
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
* BC break: Update to React/Promise 2.0
* BC break: Update to Evenement 2.0
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
* Bump React dependencies to v0.4
## 0.3.0 (2013-04-14)
* Bump React dependencies to v0.3
## 0.2.6 (2012-12-26)
* Bug fix: Emit end event when Response closes (@beaucollins)
## 0.2.3 (2012-11-14)
* Bug fix: Forward drain events from HTTP response (@cs278)
* Dependency: Updated guzzle deps to `3.0.*`
## 0.2.2 (2012-10-28)
* Version bump
## 0.2.1 (2012-10-14)
* Feature: Support HTTP 1.1 continue
## 0.2.0 (2012-09-10)
* Bump React dependencies to v0.2
## 0.1.1 (2012-07-12)
* Version bump
## 0.1.0 (2012-07-11)
* First tagged release

21
vendor/react/http/LICENSE vendored Normal file
View File

@@ -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.

3024
vendor/react/http/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

57
vendor/react/http/composer.json vendored Normal file
View File

@@ -0,0 +1,57 @@
{
"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": "^3.2 || ^2.3 || ^1.2.1",
"react/socket": "^1.16",
"react/stream": "^1.4"
},
"require-dev": {
"clue/http-proxy-react": "^1.8",
"clue/reactphp-ssh-proxy": "^1.4",
"clue/socks-react": "^1.4",
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.2 || ^3 || ^2",
"react/promise-stream": "^1.4",
"react/promise-timer": "^1.11"
},
"autoload": {
"psr-4": {
"React\\Http\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Http\\": "tests/"
}
}
}

858
vendor/react/http/src/Browser.php vendored Normal file
View File

@@ -0,0 +1,858 @@
<?php
namespace React\Http;
use Psr\Http\Message\ResponseInterface;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Http\Io\Sender;
use React\Http\Io\Transaction;
use React\Http\Message\Request;
use React\Http\Message\Uri;
use React\Promise\PromiseInterface;
use React\Socket\Connector;
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';
private $defaultHeaders = array(
'User-Agent' => 'ReactPHP/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 ?: new Connector(array(), $loop)),
$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>
*/
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>
*/
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
));
}
/**
* Add a request header for all following requests.
*
* ```php
* $browser = $browser->withHeader('User-Agent', 'ACME');
*
* $browser->get($url)->then(…);
* ```
*
* Note that the new header will overwrite any headers previously set with
* the same name (case-insensitive). Following requests will use these headers
* by default unless they are explicitly set for any requests.
*
* @param string $header
* @param string $value
* @return Browser
*/
public function withHeader($header, $value)
{
$browser = $this->withoutHeader($header);
$browser->defaultHeaders[$header] = $value;
return $browser;
}
/**
* Remove any default request headers previously set via
* the [`withHeader()` method](#withheader).
*
* ```php
* $browser = $browser->withoutHeader('User-Agent');
*
* $browser->get($url)->then(…);
* ```
*
* Note that this method only affects the headers which were set with the
* method `withHeader(string $header, string $value): Browser`
*
* @param string $header
* @return Browser
*/
public function withoutHeader($header)
{
$browser = clone $this;
/** @var string|int $key */
foreach (\array_keys($browser->defaultHeaders) as $key) {
if (\strcasecmp($key, $header) === 0) {
unset($browser->defaultHeaders[$key]);
break;
}
}
return $browser;
}
/**
* 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>
*/
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, new Uri($url));
}
foreach ($this->defaultHeaders as $key => $value) {
$explicitHeaderExists = false;
foreach (\array_keys($headers) as $headerKey) {
if (\strcasecmp($headerKey, $key) === 0) {
$explicitHeaderExists = true;
break;
}
}
if (!$explicitHeaderExists) {
$headers[$key] = $value;
}
}
return $this->transaction->send(
new Request($method, $url, $headers, $body, $this->protocolVersion)
);
}
}

27
vendor/react/http/src/Client/Client.php vendored Normal file
View File

@@ -0,0 +1,27 @@
<?php
namespace React\Http\Client;
use Psr\Http\Message\RequestInterface;
use React\Http\Io\ClientConnectionManager;
use React\Http\Io\ClientRequestStream;
/**
* @internal
*/
class Client
{
/** @var ClientConnectionManager */
private $connectionManager;
public function __construct(ClientConnectionManager $connectionManager)
{
$this->connectionManager = $connectionManager;
}
/** @return ClientRequestStream */
public function request(RequestInterface $request)
{
return new ClientRequestStream($this->connectionManager, $request);
}
}

351
vendor/react/http/src/HttpServer.php vendored Normal file
View File

@@ -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;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* [Internal] Abstract HTTP message base class (PSR-7)
*
* @internal
* @see MessageInterface
*/
abstract class AbstractMessage implements MessageInterface
{
/**
* [Internal] Regex used to match all request header fields into an array, thanks to @kelunik for checking the HTTP specs and coming up with this regex
*
* @internal
* @var string
*/
const REGEX_HEADERS = '/^([^()<>@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m';
/** @var array<string,string[]> */
private $headers = array();
/** @var array<string,string> */
private $headerNamesLowerCase = array();
/** @var string */
private $protocolVersion;
/** @var StreamInterface */
private $body;
/**
* @param string $protocolVersion
* @param array<string,string|string[]> $headers
* @param StreamInterface $body
*/
protected function __construct($protocolVersion, array $headers, StreamInterface $body)
{
foreach ($headers as $name => $value) {
if ($value !== array()) {
if (\is_array($value)) {
foreach ($value as &$one) {
$one = (string) $one;
}
} else {
$value = array((string) $value);
}
$lower = \strtolower($name);
if (isset($this->headerNamesLowerCase[$lower])) {
$value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value);
unset($this->headers[$this->headerNamesLowerCase[$lower]]);
}
$this->headers[$name] = $value;
$this->headerNamesLowerCase[$lower] = $name;
}
}
$this->protocolVersion = (string) $protocolVersion;
$this->body = $body;
}
public function getProtocolVersion()
{
return $this->protocolVersion;
}
public function withProtocolVersion($version)
{
if ((string) $version === $this->protocolVersion) {
return $this;
}
$message = clone $this;
$message->protocolVersion = (string) $version;
return $message;
}
public function getHeaders()
{
return $this->headers;
}
public function hasHeader($name)
{
return isset($this->headerNamesLowerCase[\strtolower($name)]);
}
public function getHeader($name)
{
$lower = \strtolower($name);
return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array();
}
public function getHeaderLine($name)
{
return \implode(', ', $this->getHeader($name));
}
public function withHeader($name, $value)
{
if ($value === array()) {
return $this->withoutHeader($name);
} elseif (\is_array($value)) {
foreach ($value as &$one) {
$one = (string) $one;
}
} else {
$value = array((string) $value);
}
$lower = \strtolower($name);
if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) {
return $this;
}
$message = clone $this;
if (isset($message->headerNamesLowerCase[$lower])) {
unset($message->headers[$message->headerNamesLowerCase[$lower]]);
}
$message->headers[$name] = $value;
$message->headerNamesLowerCase[$lower] = $name;
return $message;
}
public function withAddedHeader($name, $value)
{
if ($value === array()) {
return $this;
}
return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value)));
}
public function withoutHeader($name)
{
$lower = \strtolower($name);
if (!isset($this->headerNamesLowerCase[$lower])) {
return $this;
}
$message = clone $this;
unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]);
return $message;
}
public function getBody()
{
return $this->body;
}
public function withBody(StreamInterface $body)
{
if ($body === $this->body) {
return $this;
}
$message = clone $this;
$message->body = $body;
return $message;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use React\Http\Message\Uri;
/**
* [Internal] Abstract HTTP request base class (PSR-7)
*
* @internal
* @see RequestInterface
*/
abstract class AbstractRequest extends AbstractMessage implements RequestInterface
{
/** @var ?string */
private $requestTarget;
/** @var string */
private $method;
/** @var UriInterface */
private $uri;
/**
* @param string $method
* @param string|UriInterface $uri
* @param array<string,string|string[]> $headers
* @param StreamInterface $body
* @param string unknown $protocolVersion
*/
protected function __construct(
$method,
$uri,
array $headers,
StreamInterface $body,
$protocolVersion
) {
if (\is_string($uri)) {
$uri = new Uri($uri);
} elseif (!$uri instanceof UriInterface) {
throw new \InvalidArgumentException(
'Argument #2 ($uri) expected string|Psr\Http\Message\UriInterface'
);
}
// assign default `Host` request header from URI unless already given explicitly
$host = $uri->getHost();
if ($host !== '') {
foreach ($headers as $name => $value) {
if (\strtolower($name) === 'host' && $value !== array()) {
$host = '';
break;
}
}
if ($host !== '') {
$port = $uri->getPort();
if ($port !== null && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
$host .= ':' . $port;
}
$headers = array('Host' => $host) + $headers;
}
}
parent::__construct($protocolVersion, $headers, $body);
$this->method = $method;
$this->uri = $uri;
}
public function getRequestTarget()
{
if ($this->requestTarget !== null) {
return $this->requestTarget;
}
$target = $this->uri->getPath();
if ($target === '') {
$target = '/';
}
if (($query = $this->uri->getQuery()) !== '') {
$target .= '?' . $query;
}
return $target;
}
public function withRequestTarget($requestTarget)
{
if ((string) $requestTarget === $this->requestTarget) {
return $this;
}
$request = clone $this;
$request->requestTarget = (string) $requestTarget;
return $request;
}
public function getMethod()
{
return $this->method;
}
public function withMethod($method)
{
if ((string) $method === $this->method) {
return $this;
}
$request = clone $this;
$request->method = (string) $method;
return $request;
}
public function getUri()
{
return $this->uri;
}
public function withUri(UriInterface $uri, $preserveHost = false)
{
if ($uri === $this->uri) {
return $this;
}
$request = clone $this;
$request->uri = $uri;
$host = $uri->getHost();
$port = $uri->getPort();
if ($port !== null && $host !== '' && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) {
$host .= ':' . $port;
}
// update `Host` request header if URI contains a new host and `$preserveHost` is false
if ($host !== '' && (!$preserveHost || $request->getHeaderLine('Host') === '')) {
// first remove all headers before assigning `Host` header to ensure it always comes first
foreach (\array_keys($request->getHeaders()) as $name) {
$request = $request->withoutHeader($name);
}
// add `Host` header first, then all other original headers
$request = $request->withHeader('Host', $host);
foreach ($this->withoutHeader('Host')->getHeaders() as $name => $value) {
$request = $request->withHeader($name, $value);
}
}
return $request;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\UriInterface;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Promise\PromiseInterface;
use React\Socket\ConnectionInterface;
use React\Socket\ConnectorInterface;
/**
* [Internal] Manages outgoing HTTP connections for the HTTP client
*
* @internal
* @final
*/
class ClientConnectionManager
{
/** @var ConnectorInterface */
private $connector;
/** @var LoopInterface */
private $loop;
/** @var string[] */
private $idleUris = array();
/** @var ConnectionInterface[] */
private $idleConnections = array();
/** @var TimerInterface[] */
private $idleTimers = array();
/** @var \Closure[] */
private $idleStreamHandlers = array();
/** @var float */
private $maximumTimeToKeepAliveIdleConnection = 0.001;
public function __construct(ConnectorInterface $connector, LoopInterface $loop)
{
$this->connector = $connector;
$this->loop = $loop;
}
/**
* @return PromiseInterface<ConnectionInterface>
*/
public function connect(UriInterface $uri)
{
$scheme = $uri->getScheme();
if ($scheme !== 'https' && $scheme !== 'http') {
return \React\Promise\reject(new \InvalidArgumentException(
'Invalid request URL given'
));
}
$port = $uri->getPort();
if ($port === null) {
$port = $scheme === 'https' ? 443 : 80;
}
$uri = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port;
// Reuse idle connection for same URI if available
foreach ($this->idleConnections as $id => $connection) {
if ($this->idleUris[$id] === $uri) {
assert($this->idleStreamHandlers[$id] instanceof \Closure);
$connection->removeListener('close', $this->idleStreamHandlers[$id]);
$connection->removeListener('data', $this->idleStreamHandlers[$id]);
$connection->removeListener('error', $this->idleStreamHandlers[$id]);
assert($this->idleTimers[$id] instanceof TimerInterface);
$this->loop->cancelTimer($this->idleTimers[$id]);
unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]);
return \React\Promise\resolve($connection);
}
}
// Create new connection if no idle connection to same URI is available
return $this->connector->connect($uri);
}
/**
* Hands back an idle connection to the connection manager for possible future reuse.
*
* @return void
*/
public function keepAlive(UriInterface $uri, ConnectionInterface $connection)
{
$scheme = $uri->getScheme();
assert($scheme === 'https' || $scheme === 'http');
$port = $uri->getPort();
if ($port === null) {
$port = $scheme === 'https' ? 443 : 80;
}
$this->idleUris[] = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port;
$this->idleConnections[] = $connection;
$that = $this;
$cleanUp = function () use ($connection, $that) {
// call public method to support legacy PHP 5.3
$that->cleanUpConnection($connection);
};
// clean up and close connection when maximum time to keep-alive idle connection has passed
$this->idleTimers[] = $this->loop->addTimer($this->maximumTimeToKeepAliveIdleConnection, $cleanUp);
// clean up and close connection when unexpected close/data/error event happens during idle time
$this->idleStreamHandlers[] = $cleanUp;
$connection->on('close', $cleanUp);
$connection->on('data', $cleanUp);
$connection->on('error', $cleanUp);
}
/**
* @internal
* @return void
*/
public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+)
{
$id = \array_search($connection, $this->idleConnections, true);
if ($id === false) {
return;
}
assert(\is_int($id));
assert($this->idleTimers[$id] instanceof TimerInterface);
$this->loop->cancelTimer($this->idleTimers[$id]);
unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]);
$connection->close();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace React\Http\Io;
/** @internal */
class ClientRequestState
{
/** @var int */
public $numRequests = 0;
/** @var ?\React\Promise\PromiseInterface */
public $pending = null;
/** @var ?\React\EventLoop\TimerInterface */
public $timeout = null;
}

View File

@@ -0,0 +1,307 @@
<?php
namespace React\Http\Io;
use Evenement\EventEmitter;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use React\Http\Message\Response;
use React\Socket\ConnectionInterface;
use React\Stream\WritableStreamInterface;
/**
* @event response
* @event drain
* @event error
* @event close
* @internal
*/
class ClientRequestStream extends EventEmitter implements WritableStreamInterface
{
const STATE_INIT = 0;
const STATE_WRITING_HEAD = 1;
const STATE_HEAD_WRITTEN = 2;
const STATE_END = 3;
/** @var ClientConnectionManager */
private $connectionManager;
/** @var RequestInterface */
private $request;
/** @var ?ConnectionInterface */
private $connection;
/** @var string */
private $buffer = '';
private $responseFactory;
private $state = self::STATE_INIT;
private $ended = false;
private $pendingWrites = '';
public function __construct(ClientConnectionManager $connectionManager, RequestInterface $request)
{
$this->connectionManager = $connectionManager;
$this->request = $request;
}
public function isWritable()
{
return self::STATE_END > $this->state && !$this->ended;
}
private function writeHead()
{
$this->state = self::STATE_WRITING_HEAD;
$expected = 0;
$headers = "{$this->request->getMethod()} {$this->request->getRequestTarget()} HTTP/{$this->request->getProtocolVersion()}\r\n";
foreach ($this->request->getHeaders() as $name => $values) {
if (\strpos($name, ':') !== false) {
$expected = -1;
break;
}
foreach ($values as $value) {
$headers .= "$name: $value\r\n";
++$expected;
}
}
/** @var array $m legacy PHP 5.3 only */
if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) {
$this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers'));
return;
}
$connectionRef = &$this->connection;
$stateRef = &$this->state;
$pendingWrites = &$this->pendingWrites;
$that = $this;
$promise = $this->connectionManager->connect($this->request->getUri());
$promise->then(
function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) {
$connectionRef = $connection;
assert($connectionRef instanceof ConnectionInterface);
$connection->on('drain', array($that, 'handleDrain'));
$connection->on('data', array($that, 'handleData'));
$connection->on('end', array($that, 'handleEnd'));
$connection->on('error', array($that, 'handleError'));
$connection->on('close', array($that, 'close'));
$more = $connection->write($headers . "\r\n" . $pendingWrites);
assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD);
$stateRef = ClientRequestStream::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->connection->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)
$eom = \strpos($this->buffer, "\r\n\r\n");
$eomLegacy = \strpos($this->buffer, "\n\n");
if ($eom !== false || $eomLegacy !== false) {
try {
if ($eom !== false && ($eomLegacy === false || $eom < $eomLegacy)) {
$response = Response::parseMessage(\substr($this->buffer, 0, $eom + 2));
$bodyChunk = (string) \substr($this->buffer, $eom + 4);
} else {
$response = Response::parseMessage(\substr($this->buffer, 0, $eomLegacy + 1));
$bodyChunk = (string) \substr($this->buffer, $eomLegacy + 2);
}
} catch (\InvalidArgumentException $exception) {
$this->closeError($exception);
return;
}
// response headers successfully received => remove listeners for connection events
$connection = $this->connection;
assert($connection instanceof ConnectionInterface);
$connection->removeListener('drain', array($this, 'handleDrain'));
$connection->removeListener('data', array($this, 'handleData'));
$connection->removeListener('end', array($this, 'handleEnd'));
$connection->removeListener('error', array($this, 'handleError'));
$connection->removeListener('close', array($this, 'close'));
$this->connection = null;
$this->buffer = '';
// take control over connection handling and check if we can reuse the connection once response body closes
$that = $this;
$request = $this->request;
$connectionManager = $this->connectionManager;
$successfulEndReceived = false;
$input = $body = new CloseProtectionStream($connection);
$input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) {
// only reuse connection after successful response and both request and response allow keep alive
if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) {
$connectionManager->keepAlive($request->getUri(), $connection);
} else {
$connection->close();
}
$that->close();
});
// determine length of response body
$length = null;
$code = $response->getStatusCode();
if ($this->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');
}
$response = $response->withBody($body = new ReadableBodyStream($body, $length));
$body->on('end', function () use (&$successfulEndReceived) {
$successfulEndReceived = true;
});
// emit response with streaming response body (see `Sender`)
$this->emit('response', array($response, $body));
// re-emit HTTP response body to trigger body parsing if parts of it are buffered
if ($bodyChunk !== '') {
$input->handleData($bodyChunk);
} elseif ($length === 0) {
$input->handleEnd();
}
}
}
/** @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 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 = '';
$this->buffer = '';
if ($this->connection instanceof ConnectionInterface) {
$this->connection->close();
$this->connection = null;
}
$this->emit('close');
$this->removeAllListeners();
}
/**
* @internal
* @return bool
* @link https://www.rfc-editor.org/rfc/rfc9112#section-9.3
* @link https://www.rfc-editor.org/rfc/rfc7230#section-6.1
*/
public function hasMessageKeepAliveEnabled(MessageInterface $message)
{
// @link https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1
$connectionOptions = \array_map('trim', \explode(',', \strtolower($message->getHeaderLine('Connection'))));
if (\in_array('close', $connectionOptions, true)) {
return false;
}
if ($message->getProtocolVersion() === '1.1') {
return true;
}
if (\in_array('keep-alive', $connectionOptions, true)) {
return true;
}
return false;
}
}

54
vendor/react/http/src/Io/Clock.php vendored Normal file
View File

@@ -0,0 +1,54 @@
<?php
namespace React\Http\Io;
use React\EventLoop\LoopInterface;
/**
* [internal] Clock source that returns current timestamp and memoize clock for same tick
*
* This is mostly used as an internal optimization to avoid unneeded syscalls to
* get the current system time multiple times within the same loop tick. For the
* purpose of the HTTP server, the clock is assumed to not change to a
* significant degree within the same loop tick. If you need a high precision
* clock source, you may want to use `\hrtime()` instead (PHP 7.3+).
*
* The API is modelled to resemble the PSR-20 `ClockInterface` (in draft at the
* time of writing this), but uses a `float` return value for performance
* reasons instead.
*
* Note that this is an internal class only and nothing you should usually care
* about for outside use.
*
* @internal
*/
class Clock
{
/** @var LoopInterface $loop */
private $loop;
/** @var ?float */
private $now;
public function __construct(LoopInterface $loop)
{
$this->loop = $loop;
}
/** @return float */
public function now()
{
if ($this->now === null) {
$this->now = \microtime(true);
// remember clock for current loop tick only and update on next tick
$now =& $this->now;
$this->loop->futureTick(function () use (&$now) {
assert($now !== null);
$now = null;
});
}
return $this->now;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

48
vendor/react/http/src/Io/IniUtil.php vendored Normal file
View File

@@ -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;
}
}

View File

@@ -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'));
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,345 @@
<?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;
/**
* Based on $maxInputVars and $maxFileUploads
*
* @var int
*/
private $maxMultipartBodyParts;
/**
* 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 $multipartBodyPartCount = 0;
private $postCount = 0;
private $filesCount = 0;
private $emptyCount = 0;
private $cursor = 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;
$this->maxMultipartBodyParts = $this->maxInputVars + $this->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->multipartBodyPartCount = 0;
$this->cursor = 0;
$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)
$this->cursor = \strpos($buffer, $boundary . "\r\n");
while ($this->cursor !== false) {
// search following boundary (preceded by newline)
// ignore last if not followed by boundary (SHOULD end with "--")
$this->cursor += $len + 2;
$end = \strpos($buffer, "\r\n" . $boundary, $this->cursor);
if ($end === false) {
break;
}
// parse one part and continue searching for next
$this->parsePart(\substr($buffer, $this->cursor, $end - $this->cursor));
$this->cursor = $end;
if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) {
break;
}
}
}
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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -0,0 +1,179 @@
<?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;
/** @var Clock */
private $clock;
/** @var array<string|int,array<string,string>> */
private $connectionParams = array();
public function __construct(Clock $clock)
{
$this->clock = $clock;
}
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
);
} 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 ConnectionInterface $connection
* @return ServerRequestInterface
* @throws \InvalidArgumentException
* @internal
*/
public function parseRequest($headers, ConnectionInterface $connection)
{
// reuse same connection params for all server params for this connection
$cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection);
if (isset($this->connectionParams[$cid])) {
$serverParams = $this->connectionParams[$cid];
} else {
// assign new server params for new connection
$serverParams = array();
// scheme is `http` unless TLS is used
$localSocketUri = $connection->getLocalAddress();
$localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri);
if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
$serverParams['HTTPS'] = 'on';
}
// 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'];
}
// apply REMOTE_ADDR and REMOTE_PORT if source address is known
// address should always be known, unless this is over Unix domain sockets (UDS)
$remoteSocketUri = $connection->getRemoteAddress();
if ($remoteSocketUri !== null) {
$remoteAddress = \parse_url($remoteSocketUri);
$serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
$serverParams['REMOTE_PORT'] = $remoteAddress['port'];
}
// remember server params for all requests from this connection, reset on connection close
$this->connectionParams[$cid] = $serverParams;
$params =& $this->connectionParams;
$connection->on('close', function () use (&$params, $cid) {
assert(\is_array($params));
unset($params[$cid]);
});
}
// create new obj implementing ServerRequestInterface by preserving all
// previous properties and restoring original request-target
$serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now());
$serverParams['REQUEST_TIME_FLOAT'] = $now;
return ServerRequest::parseMessage($headers, $serverParams);
}
}

152
vendor/react/http/src/Io/Sender.php vendored Normal file
View File

@@ -0,0 +1,152 @@
<?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\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)
{
return new self(new HttpClient(new ClientConnectionManager($connector, $loop)));
}
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)
{
// support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already
assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true));
$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 && $size !== 0 && $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;
}
// automatically add `Authorization: Basic …` request header if URL includes `user:pass@host`
if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) {
$request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo()));
}
$requestStream = $this->http->request($request);
$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) use ($deferred, $request) {
$deferred->resolve($response);
});
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();
}
}

View File

@@ -0,0 +1,405 @@
<?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\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;
/** @var Clock */
private $clock;
/**
* 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->callback = $requestHandler;
$this->clock = new Clock($loop);
$this->parser = new RequestHeaderParser($this->clock);
$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
$connectionOnCloseResponseCancelerHandler = function () {};
if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) {
$connectionOnCloseResponseCanceler = function () use ($response) {
$response->cancel();
};
$connectionOnCloseResponseCancelerHandler = function () use ($connectionOnCloseResponseCanceler, $conn) {
if ($connectionOnCloseResponseCanceler !== null) {
$conn->removeListener('close', $connectionOnCloseResponseCanceler);
}
};
$conn->on('close', $connectionOnCloseResponseCanceler);
}
// 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);
}
)->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler);
}
/** @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', (int) $this->clock->now()) . ' 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 ($method === 'HEAD' && $response->hasHeader('Content-Length')) {
// HEAD Request: preserve explicit 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
$expected = 0;
$headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n";
foreach ($response->getHeaders() as $name => $values) {
if (\strpos($name, ':') !== false) {
$expected = -1;
break;
}
foreach ($values as $value) {
$headers .= $name . ": " . $value . "\r\n";
++$expected;
}
}
/** @var array $m legacy PHP 5.3 only */
if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) {
$this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers')));
$this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request);
return;
}
// 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);
}
}
}

330
vendor/react/http/src/Io/Transaction.php vendored Normal file
View File

@@ -0,0 +1,330 @@
<?php
namespace React\Http\Io;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use React\EventLoop\LoopInterface;
use React\Http\Message\Response;
use React\Http\Message\ResponseException;
use React\Http\Message\Uri;
use React\Promise\Deferred;
use React\Promise\Promise;
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)
{
$state = new ClientRequestState();
$deferred = new Deferred(function () use ($state) {
if ($state->pending !== null) {
$state->pending->cancel();
$state->pending = null;
}
});
// 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, $state)->then(
function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) {
if ($state->timeout !== null) {
$loop->cancelTimer($state->timeout);
$state->timeout = null;
}
$timeout = -1;
$deferred->resolve($response);
},
function ($e) use ($state, $deferred, $loop, &$timeout) {
if ($state->timeout !== null) {
$loop->cancelTimer($state->timeout);
$state->timeout = null;
}
$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, $state, &$timeout) {
if ($timeout >= 0) {
$that->applyTimeout($deferred, $state, $timeout);
}
});
} else {
$this->applyTimeout($deferred, $state, $timeout);
}
return $deferred->promise();
}
/**
* @internal
* @param number $timeout
* @return void
*/
public function applyTimeout(Deferred $deferred, ClientRequestState $state, $timeout)
{
$state->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred, $state) {
$deferred->reject(new \RuntimeException(
'Request timed out after ' . $timeout . ' seconds'
));
if ($state->pending !== null) {
$state->pending->cancel();
$state->pending = null;
}
});
}
private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
$this->progress('request', array($request));
$that = $this;
++$state->numRequests;
$promise = $this->sender->send($request);
if (!$this->streaming) {
$promise = $promise->then(function ($response) use ($deferred, $state, $that) {
return $that->bufferResponse($response, $deferred, $state);
});
}
$state->pending = $promise;
return $promise->then(
function (ResponseInterface $response) use ($request, $that, $deferred, $state) {
return $that->onResponse($response, $request, $deferred, $state);
}
);
}
/**
* @internal
* @return PromiseInterface Promise<ResponseInterface, Exception>
*/
public function bufferResponse(ResponseInterface $response, Deferred $deferred, ClientRequestState $state)
{
$body = $response->getBody();
$size = $body->getSize();
if ($size !== null && $size > $this->maximumSize) {
$body->close();
return \React\Promise\reject(new \OverflowException(
'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
// body is not streaming => already buffered
if (!$body instanceof ReadableStreamInterface) {
return \React\Promise\resolve($response);
}
/** @var ?\Closure $closer */
$closer = null;
$maximumSize = $this->maximumSize;
return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) {
// resolve with current buffer when stream closes successfully
$buffer = '';
$body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) {
$resolve($response->withBody(new BufferedBody($buffer)));
});
// buffer response body data in memory
$body->on('data', function ($data) use (&$buffer, $maximumSize, $body, $closer, $reject) {
$buffer .= $data;
// close stream and reject promise if limit is exceeded
if (isset($buffer[$maximumSize])) {
$buffer = '';
assert($closer instanceof \Closure);
$body->removeListener('close', $closer);
$body->close();
$reject(new \OverflowException(
'Response body size exceeds maximum of ' . $maximumSize . ' bytes',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
});
// reject buffering if body emits error
$body->on('error', function (\Exception $e) use ($reject) {
$reject(new \RuntimeException(
'Error while buffering response body: ' . $e->getMessage(),
$e->getCode(),
$e
));
});
}, function () use ($body, &$closer) {
// cancelled buffering: remove close handler to avoid resolving, then close and reject
assert($closer instanceof \Closure);
$body->removeListener('close', $closer);
$body->close();
throw new \RuntimeException('Cancelled buffering response body');
});
}
/**
* @internal
* @throws ResponseException
* @return ResponseInterface|PromiseInterface
*/
public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
$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, $state);
}
// 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
* @param Deferred $deferred
* @param ClientRequestState $state
* @return PromiseInterface
* @throws \RuntimeException
*/
private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state)
{
// resolve location relative to last request URI
$location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location')));
$request = $this->makeRedirectRequest($request, $location, $response->getStatusCode());
$this->progress('redirect', array($request));
if ($state->numRequests >= $this->maxRedirects) {
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
}
return $this->next($request, $deferred, $state);
}
/**
* @param RequestInterface $request
* @param UriInterface $location
* @param int $statusCode
* @return RequestInterface
* @throws \RuntimeException
*/
private function makeRedirectRequest(RequestInterface $request, UriInterface $location, $statusCode)
{
// Remove authorization if changing hostnames (but not if just changing ports or protocols).
$originalHost = $request->getUri()->getHost();
if ($location->getHost() !== $originalHost) {
$request = $request->withoutHeader('Authorization');
}
$request = $request->withoutHeader('Host')->withUri($location);
if ($statusCode === Response::STATUS_TEMPORARY_REDIRECT || $statusCode === Response::STATUS_PERMANENT_REDIRECT) {
if ($request->getBody() instanceof ReadableStreamInterface) {
throw new \RuntimeException('Unable to redirect request with streaming body');
}
} else {
$request = $request
->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET')
->withoutHeader('Content-Type')
->withoutHeader('Content-Length')
->withBody(new BufferedBody(''));
}
return $request;
}
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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace React\Http\Message;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use React\Http\Io\AbstractRequest;
use React\Http\Io\BufferedBody;
use React\Http\Io\ReadableBodyStream;
use React\Stream\ReadableStreamInterface;
/**
* Respresents an outgoing HTTP request message.
*
* This class implements the
* [PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface)
* which extends the
* [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface).
*
* This is mostly used internally to represent each outgoing HTTP request
* message for the HTTP client implementation. Likewise, you can also use this
* class with other HTTP client implementations and for tests.
*
* > Internally, this implementation builds on top of a base class which is
* considered an implementation detail that may change in the future.
*
* @see RequestInterface
*/
final class Request extends AbstractRequest implements RequestInterface
{
/**
* @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.
* @throws \InvalidArgumentException for an invalid URL or body
*/
public function __construct(
$method,
$url,
array $headers = array(),
$body = '',
$version = '1.1'
) {
if (\is_string($body)) {
$body = new BufferedBody($body);
} elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) {
$body = new ReadableBodyStream($body);
} elseif (!$body instanceof StreamInterface) {
throw new \InvalidArgumentException('Invalid request body given');
}
parent::__construct($method, $url, $headers, $body, $version);
}
}

View File

@@ -0,0 +1,414 @@
<?php
namespace React\Http\Message;
use Fig\Http\Message\StatusCodeInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use React\Http\Io\AbstractMessage;
use React\Http\Io\BufferedBody;
use React\Http\Io\HttpBodyStream;
use React\Stream\ReadableStreamInterface;
/**
* 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 a base class which is
* considered an implementation detail that may change in the future.
*
* @see \Psr\Http\Message\ResponseInterface
*/
final class Response extends AbstractMessage implements ResponseInterface, 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);
}
/**
* @var bool
* @see self::$phrasesMap
*/
private static $phrasesInitialized = false;
/**
* Map of standard HTTP status codes to standard reason phrases.
*
* This map will be fully populated with all standard reason phrases on
* first access. By default, it only contains a subset of HTTP status codes
* that have a custom mapping to reason phrases (such as those with dashes
* and all caps words). See `self::STATUS_*` for all possible status code
* constants.
*
* @var array<int,string>
* @see self::STATUS_*
* @see self::getReasonPhraseForStatusCode()
*/
private static $phrasesMap = array(
200 => 'OK',
203 => 'Non-Authoritative Information',
207 => 'Multi-Status',
226 => 'IM Used',
414 => 'URI Too Large',
418 => 'I\'m a teapot',
505 => 'HTTP Version Not Supported'
);
/** @var int */
private $statusCode;
/** @var string */
private $reasonPhrase;
/**
* @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($version, $headers, $body);
$this->statusCode = (int) $status;
$this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status);
}
public function getStatusCode()
{
return $this->statusCode;
}
public function withStatus($code, $reasonPhrase = '')
{
if ((string) $reasonPhrase === '') {
$reasonPhrase = self::getReasonPhraseForStatusCode($code);
}
if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) {
return $this;
}
$response = clone $this;
$response->statusCode = (int) $code;
$response->reasonPhrase = (string) $reasonPhrase;
return $response;
}
public function getReasonPhrase()
{
return $this->reasonPhrase;
}
/**
* @param int $code
* @return string default reason phrase for given status code or empty string if unknown
*/
private static function getReasonPhraseForStatusCode($code)
{
if (!self::$phrasesInitialized) {
self::$phrasesInitialized = true;
// map all `self::STATUS_` constants from status code to reason phrase
// e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found`
$ref = new \ReflectionClass(__CLASS__);
foreach ($ref->getConstants() as $name => $value) {
if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) {
self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7))));
}
}
}
return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : '';
}
/**
* [Internal] Parse incoming HTTP protocol message
*
* @internal
* @param string $message
* @return self
* @throws \InvalidArgumentException if given $message is not a valid HTTP response message
*/
public static function parseMessage($message)
{
$start = array();
if (!\preg_match('#^HTTP/(?<version>\d\.\d) (?<status>\d{3})(?: (?<reason>[^\r\n]*+))?[\r]?+\n#m', $message, $start)) {
throw new \InvalidArgumentException('Unable to parse invalid status-line');
}
// only support HTTP/1.1 and HTTP/1.0 requests
if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
throw new \InvalidArgumentException('Received response with invalid protocol version');
}
// check number of valid header fields matches number of lines + status line
$matches = array();
$n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
if (\substr_count($message, "\n") !== $n + 1) {
throw new \InvalidArgumentException('Unable to parse invalid response header fields');
}
// format all header fields into associative array
$headers = array();
foreach ($matches as $match) {
$headers[$match[1]][] = $match[2];
}
return new self(
(int) $start['status'],
$headers,
'',
$start['version'],
isset($start['reason']) ? $start['reason'] : ''
);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,331 @@
<?php
namespace React\Http\Message;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use React\Http\Io\AbstractRequest;
use React\Http\Io\BufferedBody;
use React\Http\Io\HttpBodyStream;
use React\Stream\ReadableStreamInterface;
/**
* 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 a base class which is
* considered an implementation detail that may change in the future.
*
* @see ServerRequestInterface
*/
final class ServerRequest extends AbstractRequest 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()
) {
if (\is_string($body)) {
$body = new BufferedBody($body);
} elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) {
$temp = new self($method, '', $headers);
$size = (int) $temp->getHeaderLine('Content-Length');
if (\strtolower($temp->getHeaderLine('Transfer-Encoding')) === 'chunked') {
$size = null;
}
$body = new HttpBodyStream($body, $size);
} elseif (!$body instanceof StreamInterface) {
throw new \InvalidArgumentException('Invalid server request body given');
}
parent::__construct($method, $url, $headers, $body, $version);
$this->serverParams = $serverParams;
$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 = $nameValuePair[0];
$value = \urldecode($nameValuePair[1]);
$result[$key] = $value;
}
}
return $result;
}
/**
* [Internal] Parse incoming HTTP protocol message
*
* @internal
* @param string $message
* @param array<string,string|int|float> $serverParams
* @return self
* @throws \InvalidArgumentException if given $message is not a valid HTTP request message
*/
public static function parseMessage($message, array $serverParams)
{
// parse request line like "GET /path HTTP/1.1"
$start = array();
if (!\preg_match('#^(?<method>[^ ]+) (?<target>[^ ]+) HTTP/(?<version>\d\.\d)#m', $message, $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);
}
// check number of valid header fields matches number of lines + request line
$matches = array();
$n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER);
if (\substr_count($message, "\n") !== $n + 1) {
throw new \InvalidArgumentException('Unable to parse invalid request header fields');
}
// format all header fields into associative array
$host = null;
$headers = array();
foreach ($matches as $match) {
$headers[$match[1]][] = $match[2];
// match `Host` request header
if ($host === null && \strtolower($match[1]) === 'host') {
$host = $match[2];
}
}
// scheme is `http` unless TLS is used
$scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://';
// default host if unset comes from local socket address or defaults to localhost
$hasHost = $host !== null;
if ($host === null) {
$host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_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'];
}
}
$request = new self(
$start['method'],
$uri,
$headers,
'',
$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;
}
}

356
vendor/react/http/src/Message/Uri.php vendored Normal file
View File

@@ -0,0 +1,356 @@
<?php
namespace React\Http\Message;
use Psr\Http\Message\UriInterface;
/**
* Respresents a URI (or URL).
*
* This class implements the
* [PSR-7 `UriInterface`](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface).
*
* This is mostly used internally to represent the URI of each HTTP request
* message for our HTTP client and server implementations. Likewise, you may
* also use this class with other HTTP implementations and for tests.
*
* @see UriInterface
*/
final class Uri implements UriInterface
{
/** @var string */
private $scheme = '';
/** @var string */
private $userInfo = '';
/** @var string */
private $host = '';
/** @var ?int */
private $port = null;
/** @var string */
private $path = '';
/** @var string */
private $query = '';
/** @var string */
private $fragment = '';
/**
* @param string $uri
* @throws \InvalidArgumentException if given $uri is invalid
*/
public function __construct($uri)
{
// @codeCoverageIgnoreStart
if (\PHP_VERSION_ID < 50407 && \strpos($uri, '//') === 0) {
// @link https://3v4l.org/UrAQP
$parts = \parse_url('http:' . $uri);
unset($parts['schema']);
} else {
$parts = \parse_url($uri);
}
// @codeCoverageIgnoreEnd
if ($parts === false || (isset($parts['scheme']) && !\preg_match('#^[a-z]+$#i', $parts['scheme'])) || (isset($parts['host']) && \preg_match('#[\s%+]#', $parts['host']))) {
throw new \InvalidArgumentException('Invalid URI given');
}
if (isset($parts['scheme'])) {
$this->scheme = \strtolower($parts['scheme']);
}
if (isset($parts['user']) || isset($parts['pass'])) {
$this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : '');
}
if (isset($parts['host'])) {
$this->host = \strtolower($parts['host']);
}
if (isset($parts['port']) && !(($parts['port'] === 80 && $this->scheme === 'http') || ($parts['port'] === 443 && $this->scheme === 'https'))) {
$this->port = $parts['port'];
}
if (isset($parts['path'])) {
$this->path = $this->encode($parts['path'], \PHP_URL_PATH);
}
if (isset($parts['query'])) {
$this->query = $this->encode($parts['query'], \PHP_URL_QUERY);
}
if (isset($parts['fragment'])) {
$this->fragment = $this->encode($parts['fragment'], \PHP_URL_FRAGMENT);
}
}
public function getScheme()
{
return $this->scheme;
}
public function getAuthority()
{
if ($this->host === '') {
return '';
}
return ($this->userInfo !== '' ? $this->userInfo . '@' : '') . $this->host . ($this->port !== null ? ':' . $this->port : '');
}
public function getUserInfo()
{
return $this->userInfo;
}
public function getHost()
{
return $this->host;
}
public function getPort()
{
return $this->port;
}
public function getPath()
{
return $this->path;
}
public function getQuery()
{
return $this->query;
}
public function getFragment()
{
return $this->fragment;
}
public function withScheme($scheme)
{
$scheme = \strtolower($scheme);
if ($scheme === $this->scheme) {
return $this;
}
if (!\preg_match('#^[a-z]*$#', $scheme)) {
throw new \InvalidArgumentException('Invalid URI scheme given');
}
$new = clone $this;
$new->scheme = $scheme;
if (($this->port === 80 && $scheme === 'http') || ($this->port === 443 && $scheme === 'https')) {
$new->port = null;
}
return $new;
}
public function withUserInfo($user, $password = null)
{
$userInfo = $this->encode($user, \PHP_URL_USER) . ($password !== null ? ':' . $this->encode($password, \PHP_URL_PASS) : '');
if ($userInfo === $this->userInfo) {
return $this;
}
$new = clone $this;
$new->userInfo = $userInfo;
return $new;
}
public function withHost($host)
{
$host = \strtolower($host);
if ($host === $this->host) {
return $this;
}
if (\preg_match('#[\s%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) {
throw new \InvalidArgumentException('Invalid URI host given');
}
$new = clone $this;
$new->host = $host;
return $new;
}
public function withPort($port)
{
$port = $port === null ? null : (int) $port;
if (($port === 80 && $this->scheme === 'http') || ($port === 443 && $this->scheme === 'https')) {
$port = null;
}
if ($port === $this->port) {
return $this;
}
if ($port !== null && ($port < 1 || $port > 0xffff)) {
throw new \InvalidArgumentException('Invalid URI port given');
}
$new = clone $this;
$new->port = $port;
return $new;
}
public function withPath($path)
{
$path = $this->encode($path, \PHP_URL_PATH);
if ($path === $this->path) {
return $this;
}
$new = clone $this;
$new->path = $path;
return $new;
}
public function withQuery($query)
{
$query = $this->encode($query, \PHP_URL_QUERY);
if ($query === $this->query) {
return $this;
}
$new = clone $this;
$new->query = $query;
return $new;
}
public function withFragment($fragment)
{
$fragment = $this->encode($fragment, \PHP_URL_FRAGMENT);
if ($fragment === $this->fragment) {
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
public function __toString()
{
$uri = '';
if ($this->scheme !== '') {
$uri .= $this->scheme . ':';
}
$authority = $this->getAuthority();
if ($authority !== '') {
$uri .= '//' . $authority;
}
if ($authority !== '' && isset($this->path[0]) && $this->path[0] !== '/') {
$uri .= '/' . $this->path;
} elseif ($authority === '' && isset($this->path[0]) && $this->path[0] === '/') {
$uri .= '/' . \ltrim($this->path, '/');
} else {
$uri .= $this->path;
}
if ($this->query !== '') {
$uri .= '?' . $this->query;
}
if ($this->fragment !== '') {
$uri .= '#' . $this->fragment;
}
return $uri;
}
/**
* @param string $part
* @param int $component
* @return string
*/
private function encode($part, $component)
{
return \preg_replace_callback(
'/(?:[^a-z0-9_\-\.~!\$&\'\(\)\*\+,;=' . ($component === \PHP_URL_PATH ? ':@\/' : ($component === \PHP_URL_QUERY || $component === \PHP_URL_FRAGMENT ? ':@\/\?' : '')) . '%]++|%(?![a-f0-9]{2}))/i',
function (array $match) {
return \rawurlencode($match[0]);
},
$part
);
}
/**
* [Internal] Resolve URI relative to base URI and return new absolute URI
*
* @internal
* @param UriInterface $base
* @param UriInterface $rel
* @return UriInterface
* @throws void
*/
public static function resolve(UriInterface $base, UriInterface $rel)
{
if ($rel->getScheme() !== '') {
return $rel->getPath() === '' ? $rel : $rel->withPath(self::removeDotSegments($rel->getPath()));
}
$reset = false;
$new = $base;
if ($rel->getAuthority() !== '') {
$reset = true;
$userInfo = \explode(':', $rel->getUserInfo(), 2);
$new = $base->withUserInfo($userInfo[0], isset($userInfo[1]) ? $userInfo[1]: null)->withHost($rel->getHost())->withPort($rel->getPort());
}
if ($reset && $rel->getPath() === '') {
$new = $new->withPath('');
} elseif (($path = $rel->getPath()) !== '') {
$start = '';
if ($path === '' || $path[0] !== '/') {
$start = $base->getPath();
if (\substr($start, -1) !== '/') {
$start .= '/../';
}
}
$reset = true;
$new = $new->withPath(self::removeDotSegments($start . $path));
}
if ($reset || $rel->getQuery() !== '') {
$reset = true;
$new = $new->withQuery($rel->getQuery());
}
if ($reset || $rel->getFragment() !== '') {
$new = $new->withFragment($rel->getFragment());
}
return $new;
}
/**
* @param string $path
* @return string
*/
private static function removeDotSegments($path)
{
$segments = array();
foreach (\explode('/', $path) as $segment) {
if ($segment === '..') {
\array_pop($segments);
} elseif ($segment !== '.' && $segment !== '') {
$segments[] = $segment;
}
}
return '/' . \implode('/', $segments) . ($path !== '/' && \substr($path, -1) === '/' ? '/' : '');
}
}

View File

@@ -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(null);
}
}

View File

@@ -0,0 +1,109 @@
<?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\Promise;
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, $next)
{
$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 || !$body->isReadable()) {
// 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 $next($request);
}
// request body of known size exceeding limit
$sizeLimit = $this->sizeLimit;
if ($size > $this->sizeLimit) {
$sizeLimit = 0;
}
/** @var ?\Closure $closer */
$closer = null;
return new Promise(function ($resolve, $reject) use ($body, &$closer, $sizeLimit, $request, $next) {
// buffer request body data in memory, discard but keep buffering if limit is reached
$buffer = '';
$bufferer = null;
$body->on('data', $bufferer = function ($data) use (&$buffer, $sizeLimit, $body, &$bufferer) {
$buffer .= $data;
// 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 (isset($buffer[$sizeLimit])) {
assert($bufferer instanceof \Closure);
$body->removeListener('data', $bufferer);
$bufferer = null;
$buffer = '';
}
});
// call $next with current buffer and resolve or reject with its results
$body->on('close', $closer = function () use (&$buffer, $request, $resolve, $reject, $next) {
try {
// resolve with result of next handler
$resolve($next($request->withBody(new BufferedBody($buffer))));
} catch (\Exception $e) {
$reject($e);
} catch (\Throwable $e) { // @codeCoverageIgnoreStart
// reject Errors just like Exceptions (PHP 7+)
$reject($e); // @codeCoverageIgnoreEnd
}
});
// reject buffering if body emits error
$body->on('error', function (\Exception $e) use ($reject, $body, $closer) {
// remove close handler to avoid resolving, then close and reject
assert($closer instanceof \Closure);
$body->removeListener('close', $closer);
$body->close();
$reject(new \RuntimeException(
'Error while buffering request body: ' . $e->getMessage(),
$e->getCode(),
$e
));
});
}, function () use ($body, &$closer) {
// cancelled buffering: remove close handler to avoid resolving, then close and reject
assert($closer instanceof \Closure);
$body->removeListener('close', $closer);
$body->close();
throw new \RuntimeException('Cancelled buffering request body');
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

18
vendor/react/http/src/Server.php vendored Normal file
View File

@@ -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
{
}
}