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

112
vendor/react/async/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,112 @@
# Changelog
## 4.3.0 (2024-06-04)
* Feature: Improve performance by avoiding unneeded references in `FiberMap`.
(#88 by @clue)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
(#87 by @clue)
* Improve type safety for test environment.
(#86 by @SimonFrings)
## 4.2.0 (2023-11-22)
* Feature: Add Promise v3 template types for all public functions.
(#40 by @WyriHaximus and @clue)
All our public APIs now use Promise v3 template types to guide IDEs and static
analysis tools (like PHPStan), helping with proper type usage and improving
code quality:
```php
assertType('bool', await(resolve(true)));
assertType('PromiseInterface<bool>', async(fn(): bool => true)());
assertType('PromiseInterface<bool>', coroutine(fn(): bool => true));
```
* Feature: Full PHP 8.3 compatibility.
(#81 by @clue)
* Update test suite to avoid unhandled promise rejections.
(#79 by @clue)
## 4.1.0 (2023-06-22)
* Feature: Add new `delay()` function to delay program execution.
(#69 and #78 by @clue)
```php
echo 'a';
Loop::addTimer(1.0, function () {
echo 'b';
});
React\Async\delay(3.0);
echo 'c';
// prints "a" at t=0.0s
// prints "b" at t=1.0s
// prints "c" at t=3.0s
```
* Update test suite, add PHPStan with `max` level and report failed assertions.
(#66 and #76 by @clue and #61 and #73 by @WyriHaximus)
## 4.0.0 (2022-07-11)
A major new feature release, see [**release announcement**](https://clue.engineering/2022/announcing-reactphp-async).
* 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.
* The v4 release will be the way forward for this package. However, we will still
actively support v3 and v2 to provide a smooth upgrade path for those not yet
on PHP 8.1+. If you're using an older PHP version, you may use either version
which all provide a compatible API but may not take advantage of newer language
features. You may target multiple versions at the same time to support a wider range of
PHP versions:
* [`4.x` branch](https://github.com/reactphp/async/tree/4.x) (PHP 8.1+)
* [`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+)
* [`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+)
This update involves some major new features and a minor BC break over the
`v3.0.0` release. 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 / BC break: Require PHP 8.1+ and add `mixed` type declarations.
(#14 by @clue)
* Feature: Add Fiber-based `async()` and `await()` functions.
(#15, #18, #19 and #20 by @WyriHaximus and #26, #28, #30, #32, #34, #55 and #57 by @clue)
* Project maintenance, rename `main` branch to `4.x` and update installation instructions.
(#29 by @clue)
The following changes had to be ported to this release due to our branching
strategy, but also appeared in the `v3.0.0` release:
* Feature: Support iterable type for `parallel()` + `series()` + `waterfall()`.
(#49 by @clue)
* Feature: Forward compatibility with upcoming Promise v3.
(#48 by @clue)
* Minor documentation improvements.
(#36 by @SimonFrings and #51 by @nhedger)
## 3.0.0 (2022-07-11)
See [`3.x` CHANGELOG](https://github.com/reactphp/async/blob/3.x/CHANGELOG.md) for more details.
## 2.0.0 (2022-07-11)
See [`2.x` CHANGELOG](https://github.com/reactphp/async/blob/2.x/CHANGELOG.md) for more details.
## 1.0.0 (2013-02-07)
* First tagged release

19
vendor/react/async/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
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.

672
vendor/react/async/README.md vendored Normal file
View File

@@ -0,0 +1,672 @@
# Async Utilities
[![CI status](https://github.com/reactphp/async/workflows/CI/badge.svg)](https://github.com/reactphp/async/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/async?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/async)
Async utilities and fibers for [ReactPHP](https://reactphp.org/).
This library allows you to manage async control flow. It provides a number of
combinators for [Promise](https://github.com/reactphp/promise)-based APIs.
Instead of nesting or chaining promise callbacks, you can declare them as a
list, which is resolved sequentially in an async manner.
React/Async will not automagically change blocking code to be async. You need
to have an actual event loop and non-blocking libraries interacting with that
event loop for it to work. As long as you have a Promise-based API that runs in
an event loop, it can be used with this library.
**Table of Contents**
* [Usage](#usage)
* [async()](#async)
* [await()](#await)
* [coroutine()](#coroutine)
* [delay()](#delay)
* [parallel()](#parallel)
* [series()](#series)
* [waterfall()](#waterfall)
* [Todo](#todo)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Usage
This lightweight library consists only of a few simple functions.
All functions reside under the `React\Async` namespace.
The below examples refer to all functions with their fully-qualified names like this:
```php
React\Async\await();
```
As of PHP 5.6+ you can also import each required function into your code like this:
```php
use function React\Async\await;
await();
```
Alternatively, you can also use an import statement similar to this:
```php
use React\Async;
Async\await();
```
### async()
The `async(callable():(PromiseInterface<T>|T) $function): (callable():PromiseInterface<T>)` function can be used to
return an async function for a function that uses [`await()`](#await) internally.
This function is specifically designed to complement the [`await()` function](#await).
The [`await()` function](#await) can be considered *blocking* from the
perspective of the calling code. You can avoid this blocking behavior by
wrapping it in an `async()` function call. Everything inside this function
will still be blocked, but everything outside this function can be executed
asynchronously without blocking:
```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
React\Async\await(React\Promise\Timer\sleep(1.0));
echo 'c';
}));
Loop::addTimer(1.0, function () {
echo 'b';
});
// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```
See also the [`await()` function](#await) for more details.
Note that this function only works in tandem with the [`await()` function](#await).
In particular, this function does not "magically" make any blocking function
non-blocking:
```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes
echo 'c';
}));
Loop::addTimer(1.0, function () {
echo 'b';
});
// prints "a" at t=0.5s
// prints "c" at t=1.5s: Correct timing, but wrong order
// prints "b" at t=1.5s: Triggered too late because it was blocked
```
As an alternative, you should always make sure to use this function in tandem
with the [`await()` function](#await) and an async API returning a promise
as shown in the previous example.
The `async()` function is specifically designed for cases where it is used
as a callback (such as an event loop timer, event listener, or promise
callback). For this reason, it returns a new function wrapping the given
`$function` instead of directly invoking it and returning its value.
```php
use function React\Async\async;
Loop::addTimer(1.0, async(function () { }));
$connection->on('close', async(function () { }));
$stream->on('data', async(function ($data) { }));
$promise->then(async(function (int $result) { }));
```
You can invoke this wrapping function to invoke the given `$function` with
any arguments given as-is. The function will always return a Promise which
will be fulfilled with whatever your `$function` returns. Likewise, it will
return a promise that will be rejected if you throw an `Exception` or
`Throwable` from your `$function`. This allows you to easily create
Promise-based functions:
```php
$promise = React\Async\async(function (): int {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$bytes = 0;
foreach ($urls as $url) {
$response = React\Async\await($browser->get($url));
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
})();
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The previous example uses [`await()`](#await) inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single [`await()`](#await) together with Promise-based
primitives like this:
```php
$promise = React\Async\async(function (): int {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}
try {
$responses = React\Async\await(React\Promise\all($promises));
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}
$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
})();
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The returned promise is implemented in such a way that it can be cancelled
when it is still pending. Cancelling a pending promise will cancel any awaited
promises inside that fiber or any nested fibers. As such, the following example
will only output `ab` and cancel the pending [`delay()`](#delay).
The [`await()`](#await) calls in this example would throw a `RuntimeException`
from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.
```php
$promise = async(static function (): int {
echo 'a';
await(async(static function (): void {
echo 'b';
delay(2);
echo 'c';
})());
echo 'd';
return time();
})();
$promise->cancel();
await($promise);
```
### await()
The `await(PromiseInterface<T> $promise): T` function can be used to
block waiting for the given `$promise` to be fulfilled.
```php
$result = React\Async\await($promise);
```
This function will only return after the given `$promise` has settled, i.e.
either fulfilled or rejected. While the promise is pending, this function
can be considered *blocking* from the perspective of the calling code.
You can avoid this blocking behavior by wrapping it in an [`async()` function](#async)
call. Everything inside this function will still be blocked, but everything
outside this function can be executed asynchronously without blocking:
```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
React\Async\await(React\Promise\Timer\sleep(1.0));
echo 'c';
}));
Loop::addTimer(1.0, function () {
echo 'b';
});
// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```
See also the [`async()` function](#async) for more details.
Once the promise is fulfilled, this function will return whatever the promise
resolved to.
Once the promise is rejected, this will throw whatever the promise rejected
with. If the promise did not reject with an `Exception` or `Throwable`, then
this function will throw an `UnexpectedValueException` instead.
```php
try {
$result = React\Async\await($promise);
// promise successfully fulfilled with $result
echo 'Result: ' . $result;
} catch (Throwable $e) {
// promise rejected with $e
echo 'Error: ' . $e->getMessage();
}
```
### coroutine()
The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface<T>|T) $function, mixed ...$args): PromiseInterface<T>` function can be used to
execute a Generator-based coroutine to "await" promises.
```php
React\Async\coroutine(function () {
$browser = new React\Http\Browser();
try {
$response = yield $browser->get('https://example.com/');
assert($response instanceof Psr\Http\Message\ResponseInterface);
echo $response->getBody();
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
});
```
Using Generator-based coroutines is an alternative to directly using the
underlying promise APIs. For many use cases, this makes using promise-based
APIs much simpler, as it resembles a synchronous code flow more closely.
The above example performs the equivalent of directly using the promise APIs:
```php
$browser = new React\Http\Browser();
$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
echo $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The `yield` keyword can be used to "await" a promise resolution. Internally,
it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
This allows the execution to be interrupted and resumed at the same place
when the promise is fulfilled. The `yield` statement returns whatever the
promise is fulfilled with. If the promise is rejected, it will throw an
`Exception` or `Throwable`.
The `coroutine()` function will always return a Promise which will be
fulfilled with whatever your `$function` returns. Likewise, it will return
a promise that will be rejected if you throw an `Exception` or `Throwable`
from your `$function`. This allows you to easily create Promise-based
functions:
```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$bytes = 0;
foreach ($urls as $url) {
$response = yield $browser->get($url);
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
The previous example uses a `yield` statement inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single `yield` together with Promise-based primitives
like this:
```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];
$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}
try {
$responses = yield React\Promise\all($promises);
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}
$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});
$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### delay()
The `delay(float $seconds): void` function can be used to
delay program execution for duration given in `$seconds`.
```php
React\Async\delay($seconds);
```
This function will only return after the given number of `$seconds` have
elapsed. If there are no other events attached to this loop, it will behave
similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
```php
echo 'a';
React\Async\delay(1.0);
echo 'b';
// prints "a" at t=0.0s
// prints "b" at t=1.0s
```
Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
this function may not necessarily halt execution of the entire process thread.
Instead, it allows the event loop to run any other events attached to the
same loop until the delay returns:
```php
echo 'a';
Loop::addTimer(1.0, function (): void {
echo 'b';
});
React\Async\delay(3.0);
echo 'c';
// prints "a" at t=0.0s
// prints "b" at t=1.0s
// prints "c" at t=3.0s
```
This behavior is especially useful if you want to delay the program execution
of a particular routine, such as when building a simple polling or retry
mechanism:
```php
try {
something();
} catch (Throwable) {
// in case of error, retry after a short delay
React\Async\delay(1.0);
something();
}
```
Because this function only returns after some time has passed, it can be
considered *blocking* from the perspective of the calling code. You can avoid
this blocking behavior by wrapping it in an [`async()` function](#async) call.
Everything inside this function will still be blocked, but everything outside
this function can be executed asynchronously without blocking:
```php
Loop::addTimer(0.5, React\Async\async(function (): void {
echo 'a';
React\Async\delay(1.0);
echo 'c';
}));
Loop::addTimer(1.0, function (): void {
echo 'b';
});
// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```
See also the [`async()` function](#async) for more details.
Internally, the `$seconds` argument will be used as a timer for the loop so that
it keeps running until this timer triggers. This implies that if you pass a
really small (or negative) value, it will still start a timer and will thus
trigger at the earliest possible time in the future.
The function is implemented in such a way that it can be cancelled when it is
running inside an [`async()` function](#async). Cancelling the resulting
promise will clean up any pending timers and throw a `RuntimeException` from
the pending delay which in turn would reject the resulting promise.
```php
$promise = async(function (): void {
echo 'a';
delay(3.0);
echo 'b';
})();
Loop::addTimer(2.0, function () use ($promise): void {
$promise->cancel();
});
// prints "a" at t=0.0s
// rejects $promise at t=2.0
// never prints "b"
```
### parallel()
The `parallel(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
like this:
```php
<?php
use React\EventLoop\Loop;
use React\Promise\Promise;
React\Async\parallel([
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for a whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for another whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for yet another whole second');
});
});
},
])->then(function (array $results) {
foreach ($results as $result) {
var_dump($result);
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### series()
The `series(iterable<callable():PromiseInterface<T>> $tasks): PromiseInterface<array<T>>` function can be used
like this:
```php
<?php
use React\EventLoop\Loop;
use React\Promise\Promise;
React\Async\series([
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for a whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for another whole second');
});
});
},
function () {
return new Promise(function ($resolve) {
Loop::addTimer(1, function () use ($resolve) {
$resolve('Slept for yet another whole second');
});
});
},
])->then(function (array $results) {
foreach ($results as $result) {
var_dump($result);
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### waterfall()
The `waterfall(iterable<callable(mixed=):PromiseInterface<T>> $tasks): PromiseInterface<T>` function can be used
like this:
```php
<?php
use React\EventLoop\Loop;
use React\Promise\Promise;
$addOne = function ($prev = 0) {
return new Promise(function ($resolve) use ($prev) {
Loop::addTimer(1, function () use ($prev, $resolve) {
$resolve($prev + 1);
});
});
};
React\Async\waterfall([
$addOne,
$addOne,
$addOne
])->then(function ($prev) {
echo "Final result is $prev\n";
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
## Todo
* Implement queue()
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version from this branch:
```bash
composer require react/async:^4.3
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on PHP 8.1+.
It's *highly recommended to use the latest supported PHP version* for this project.
We're committed to providing long-term support (LTS) options and to provide a
smooth upgrade path. If you're using an older PHP version, you may use the
[`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) or
[`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) which both
provide a compatible API but do not take advantage of newer language features.
You may target multiple versions at the same time to support a wider range of
PHP versions like this:
```bash
composer require "react/async:^4 || ^3 || ^2"
```
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
On top of this, we use PHPStan on max level to ensure type safety across the project:
```bash
vendor/bin/phpstan
```
## License
MIT, see [LICENSE file](LICENSE).
This project is heavily influenced by [async.js](https://github.com/caolan/async).

50
vendor/react/async/composer.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "react/async",
"description": "Async utilities and fibers for ReactPHP",
"keywords": ["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": ">=8.1",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.8 || ^1.2.1"
},
"require-dev": {
"phpstan/phpstan": "1.10.39",
"phpunit/phpunit": "^9.6"
},
"autoload": {
"psr-4": {
"React\\Async\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Async\\": "tests/"
}
}
}

33
vendor/react/async/src/FiberFactory.php vendored Normal file
View File

@@ -0,0 +1,33 @@
<?php
namespace React\Async;
/**
* This factory its only purpose is interoperability. Where with
* event loops one could simply wrap another event loop. But with fibers
* that has become impossible and as such we provide this factory and the
* FiberInterface.
*
* Usage is not documented and as such not supported and might chang without
* notice. Use at your own risk.
*
* @internal
*/
final class FiberFactory
{
private static ?\Closure $factory = null;
public static function create(): FiberInterface
{
return (self::factory())();
}
public static function factory(?\Closure $factory = null): \Closure
{
if ($factory !== null) {
self::$factory = $factory;
}
return self::$factory ?? static fn (): FiberInterface => new SimpleFiber();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace React\Async;
/**
* This interface its only purpose is interoperability. Where with
* event loops one could simply wrap another event loop. But with fibers
* that has become impossible and as such we provide this interface and the
* FiberFactory.
*
* Usage is not documented and as such not supported and might chang without
* notice. Use at your own risk.
*
* @internal
*/
interface FiberInterface
{
public function resume(mixed $value): void;
public function throw(\Throwable $throwable): void;
public function suspend(): mixed;
}

42
vendor/react/async/src/FiberMap.php vendored Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace React\Async;
use React\Promise\PromiseInterface;
/**
* @internal
*
* @template T
*/
final class FiberMap
{
/** @var array<int,PromiseInterface<T>> */
private static array $map = [];
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @param PromiseInterface<T> $promise
*/
public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void
{
self::$map[\spl_object_id($fiber)] = $promise;
}
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
*/
public static function unsetPromise(\Fiber $fiber): void
{
unset(self::$map[\spl_object_id($fiber)]);
}
/**
* @param \Fiber<mixed,mixed,mixed,mixed> $fiber
* @return ?PromiseInterface<T>
*/
public static function getPromise(\Fiber $fiber): ?PromiseInterface
{
return self::$map[\spl_object_id($fiber)] ?? null;
}
}

79
vendor/react/async/src/SimpleFiber.php vendored Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace React\Async;
use React\EventLoop\Loop;
/**
* @internal
*/
final class SimpleFiber implements FiberInterface
{
/** @var ?\Fiber<void,void,void,callable(): mixed> */
private static ?\Fiber $scheduler = null;
private static ?\Closure $suspend = null;
/** @var ?\Fiber<mixed,mixed,mixed,mixed> */
private ?\Fiber $fiber = null;
public function __construct()
{
$this->fiber = \Fiber::getCurrent();
}
public function resume(mixed $value): void
{
if ($this->fiber !== null) {
$this->fiber->resume($value);
} else {
self::$suspend = static fn() => $value;
}
if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) {
$suspend = self::$suspend;
self::$suspend = null;
\Fiber::suspend($suspend);
}
}
public function throw(\Throwable $throwable): void
{
if ($this->fiber !== null) {
$this->fiber->throw($throwable);
} else {
self::$suspend = static fn() => throw $throwable;
}
if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) {
$suspend = self::$suspend;
self::$suspend = null;
\Fiber::suspend($suspend);
}
}
public function suspend(): mixed
{
if ($this->fiber === null) {
if (self::$scheduler === null || self::$scheduler->isTerminated()) {
self::$scheduler = new \Fiber(static fn() => Loop::run());
// Run event loop to completion on shutdown.
\register_shutdown_function(static function (): void {
assert(self::$scheduler instanceof \Fiber);
if (self::$scheduler->isSuspended()) {
self::$scheduler->resume();
}
});
}
$ret = (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start());
assert(\is_callable($ret));
return $ret();
}
return \Fiber::suspend();
}
}

846
vendor/react/async/src/functions.php vendored Normal file
View File

@@ -0,0 +1,846 @@
<?php
namespace React\Async;
use React\EventLoop\Loop;
use React\EventLoop\TimerInterface;
use React\Promise\Deferred;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
use function React\Promise\reject;
use function React\Promise\resolve;
/**
* Return an async function for a function that uses [`await()`](#await) internally.
*
* This function is specifically designed to complement the [`await()` function](#await).
* The [`await()` function](#await) can be considered *blocking* from the
* perspective of the calling code. You can avoid this blocking behavior by
* wrapping it in an `async()` function call. Everything inside this function
* will still be blocked, but everything outside this function can be executed
* asynchronously without blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function () {
* echo 'a';
* React\Async\await(React\Promise\Timer\sleep(1.0));
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "b" at t=1.0s
* // prints "c" at t=1.5s
* ```
*
* See also the [`await()` function](#await) for more details.
*
* Note that this function only works in tandem with the [`await()` function](#await).
* In particular, this function does not "magically" make any blocking function
* non-blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function () {
* echo 'a';
* sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "c" at t=1.5s: Correct timing, but wrong order
* // prints "b" at t=1.5s: Triggered too late because it was blocked
* ```
*
* As an alternative, you should always make sure to use this function in tandem
* with the [`await()` function](#await) and an async API returning a promise
* as shown in the previous example.
*
* The `async()` function is specifically designed for cases where it is used
* as a callback (such as an event loop timer, event listener, or promise
* callback). For this reason, it returns a new function wrapping the given
* `$function` instead of directly invoking it and returning its value.
*
* ```php
* use function React\Async\async;
*
* Loop::addTimer(1.0, async(function () { … }));
* $connection->on('close', async(function () { … }));
* $stream->on('data', async(function ($data) { … }));
* $promise->then(async(function (int $result) { … }));
* ```
*
* You can invoke this wrapping function to invoke the given `$function` with
* any arguments given as-is. The function will always return a Promise which
* will be fulfilled with whatever your `$function` returns. Likewise, it will
* return a promise that will be rejected if you throw an `Exception` or
* `Throwable` from your `$function`. This allows you to easily create
* Promise-based functions:
*
* ```php
* $promise = React\Async\async(function (): int {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $bytes = 0;
* foreach ($urls as $url) {
* $response = React\Async\await($browser->get($url));
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* })();
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The previous example uses [`await()`](#await) inside a loop to highlight how
* this vastly simplifies consuming asynchronous operations. At the same time,
* this naive example does not leverage concurrent execution, as it will
* essentially "await" between each operation. In order to take advantage of
* concurrent execution within the given `$function`, you can "await" multiple
* promises by using a single [`await()`](#await) together with Promise-based
* primitives like this:
*
* ```php
* $promise = React\Async\async(function (): int {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $promises = [];
* foreach ($urls as $url) {
* $promises[] = $browser->get($url);
* }
*
* try {
* $responses = React\Async\await(React\Promise\all($promises));
* } catch (Exception $e) {
* foreach ($promises as $promise) {
* $promise->cancel();
* }
* throw $e;
* }
*
* $bytes = 0;
* foreach ($responses as $response) {
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* })();
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The returned promise is implemented in such a way that it can be cancelled
* when it is still pending. Cancelling a pending promise will cancel any awaited
* promises inside that fiber or any nested fibers. As such, the following example
* will only output `ab` and cancel the pending [`delay()`](#delay).
* The [`await()`](#await) calls in this example would throw a `RuntimeException`
* from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.
*
* ```php
* $promise = async(static function (): int {
* echo 'a';
* await(async(static function (): void {
* echo 'b';
* delay(2);
* echo 'c';
* })());
* echo 'd';
*
* return time();
* })();
*
* $promise->cancel();
* await($promise);
* ```
*
* @template T
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
* @template A2
* @template A3
* @template A4
* @template A5
* @param callable(A1,A2,A3,A4,A5): (PromiseInterface<T>|T) $function
* @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface<T>
* @since 4.0.0
* @see coroutine()
*/
function async(callable $function): callable
{
return static function (mixed ...$args) use ($function): PromiseInterface {
$fiber = null;
/** @var PromiseInterface<T> $promise*/
$promise = new Promise(function (callable $resolve, callable $reject) use ($function, $args, &$fiber): void {
$fiber = new \Fiber(function () use ($resolve, $reject, $function, $args, &$fiber): void {
try {
$resolve($function(...$args));
} catch (\Throwable $exception) {
$reject($exception);
} finally {
assert($fiber instanceof \Fiber);
FiberMap::unsetPromise($fiber);
}
});
$fiber->start();
}, function () use (&$fiber): void {
assert($fiber instanceof \Fiber);
$promise = FiberMap::getPromise($fiber);
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
});
$lowLevelFiber = \Fiber::getCurrent();
if ($lowLevelFiber !== null) {
FiberMap::setPromise($lowLevelFiber, $promise);
}
return $promise;
};
}
/**
* Block waiting for the given `$promise` to be fulfilled.
*
* ```php
* $result = React\Async\await($promise);
* ```
*
* This function will only return after the given `$promise` has settled, i.e.
* either fulfilled or rejected. While the promise is pending, this function
* can be considered *blocking* from the perspective of the calling code.
* You can avoid this blocking behavior by wrapping it in an [`async()` function](#async)
* call. Everything inside this function will still be blocked, but everything
* outside this function can be executed asynchronously without blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function () {
* echo 'a';
* React\Async\await(React\Promise\Timer\sleep(1.0));
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function () {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "b" at t=1.0s
* // prints "c" at t=1.5s
* ```
*
* See also the [`async()` function](#async) for more details.
*
* Once the promise is fulfilled, this function will return whatever the promise
* resolved to.
*
* Once the promise is rejected, this will throw whatever the promise rejected
* with. If the promise did not reject with an `Exception` or `Throwable`, then
* this function will throw an `UnexpectedValueException` instead.
*
* ```php
* try {
* $result = React\Async\await($promise);
* // promise successfully fulfilled with $result
* echo 'Result: ' . $result;
* } catch (Throwable $e) {
* // promise rejected with $e
* echo 'Error: ' . $e->getMessage();
* }
* ```
*
* @template T
* @param PromiseInterface<T> $promise
* @return T returns whatever the promise resolves to
* @throws \Exception when the promise is rejected with an `Exception`
* @throws \Throwable when the promise is rejected with a `Throwable`
* @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only)
*/
function await(PromiseInterface $promise): mixed
{
$fiber = null;
$resolved = false;
$rejected = false;
/** @var T $resolvedValue */
$resolvedValue = null;
$rejectedThrowable = null;
$lowLevelFiber = \Fiber::getCurrent();
$promise->then(
function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFiber): void {
if ($lowLevelFiber !== null) {
FiberMap::unsetPromise($lowLevelFiber);
}
/** @var ?\Fiber<mixed,mixed,mixed,mixed> $fiber */
if ($fiber === null) {
$resolved = true;
/** @var T $resolvedValue */
$resolvedValue = $value;
return;
}
$fiber->resume($value);
},
function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowLevelFiber): void {
if ($lowLevelFiber !== null) {
FiberMap::unsetPromise($lowLevelFiber);
}
if (!$throwable instanceof \Throwable) {
$throwable = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */
);
// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$trace = $r->getValue($throwable);
assert(\is_array($trace));
// Exception trace arguments only available when zend.exception_ignore_args is not set
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($throwable, $trace);
}
if ($fiber === null) {
$rejected = true;
$rejectedThrowable = $throwable;
return;
}
$fiber->throw($throwable);
}
);
if ($resolved) {
return $resolvedValue;
}
if ($rejected) {
assert($rejectedThrowable instanceof \Throwable);
throw $rejectedThrowable;
}
if ($lowLevelFiber !== null) {
FiberMap::setPromise($lowLevelFiber, $promise);
}
$fiber = FiberFactory::create();
return $fiber->suspend();
}
/**
* Delay program execution for duration given in `$seconds`.
*
* ```php
* React\Async\delay($seconds);
* ```
*
* This function will only return after the given number of `$seconds` have
* elapsed. If there are no other events attached to this loop, it will behave
* similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).
*
* ```php
* echo 'a';
* React\Async\delay(1.0);
* echo 'b';
*
* // prints "a" at t=0.0s
* // prints "b" at t=1.0s
* ```
*
* Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
* this function may not necessarily halt execution of the entire process thread.
* Instead, it allows the event loop to run any other events attached to the
* same loop until the delay returns:
*
* ```php
* echo 'a';
* Loop::addTimer(1.0, function (): void {
* echo 'b';
* });
* React\Async\delay(3.0);
* echo 'c';
*
* // prints "a" at t=0.0s
* // prints "b" at t=1.0s
* // prints "c" at t=3.0s
* ```
*
* This behavior is especially useful if you want to delay the program execution
* of a particular routine, such as when building a simple polling or retry
* mechanism:
*
* ```php
* try {
* something();
* } catch (Throwable) {
* // in case of error, retry after a short delay
* React\Async\delay(1.0);
* something();
* }
* ```
*
* Because this function only returns after some time has passed, it can be
* considered *blocking* from the perspective of the calling code. You can avoid
* this blocking behavior by wrapping it in an [`async()` function](#async) call.
* Everything inside this function will still be blocked, but everything outside
* this function can be executed asynchronously without blocking:
*
* ```php
* Loop::addTimer(0.5, React\Async\async(function (): void {
* echo 'a';
* React\Async\delay(1.0);
* echo 'c';
* }));
*
* Loop::addTimer(1.0, function (): void {
* echo 'b';
* });
*
* // prints "a" at t=0.5s
* // prints "b" at t=1.0s
* // prints "c" at t=1.5s
* ```
*
* See also the [`async()` function](#async) for more details.
*
* Internally, the `$seconds` argument will be used as a timer for the loop so that
* it keeps running until this timer triggers. This implies that if you pass a
* really small (or negative) value, it will still start a timer and will thus
* trigger at the earliest possible time in the future.
*
* The function is implemented in such a way that it can be cancelled when it is
* running inside an [`async()` function](#async). Cancelling the resulting
* promise will clean up any pending timers and throw a `RuntimeException` from
* the pending delay which in turn would reject the resulting promise.
*
* ```php
* $promise = async(function (): void {
* echo 'a';
* delay(3.0);
* echo 'b';
* })();
*
* Loop::addTimer(2.0, function () use ($promise): void {
* $promise->cancel();
* });
*
* // prints "a" at t=0.0s
* // rejects $promise at t=2.0
* // never prints "b"
* ```
*
* @return void
* @throws \RuntimeException when the function is cancelled inside an `async()` function
* @see async()
* @uses await()
*/
function delay(float $seconds): void
{
/** @var ?TimerInterface $timer */
$timer = null;
await(new Promise(function (callable $resolve) use ($seconds, &$timer): void {
$timer = Loop::addTimer($seconds, fn() => $resolve(null));
}, function () use (&$timer): void {
assert($timer instanceof TimerInterface);
Loop::cancelTimer($timer);
throw new \RuntimeException('Delay cancelled');
}));
}
/**
* Execute a Generator-based coroutine to "await" promises.
*
* ```php
* React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
*
* try {
* $response = yield $browser->get('https://example.com/');
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* echo $response->getBody();
* } catch (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* }
* });
* ```
*
* Using Generator-based coroutines is an alternative to directly using the
* underlying promise APIs. For many use cases, this makes using promise-based
* APIs much simpler, as it resembles a synchronous code flow more closely.
* The above example performs the equivalent of directly using the promise APIs:
*
* ```php
* $browser = new React\Http\Browser();
*
* $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
* echo $response->getBody();
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The `yield` keyword can be used to "await" a promise resolution. Internally,
* it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
* This allows the execution to be interrupted and resumed at the same place
* when the promise is fulfilled. The `yield` statement returns whatever the
* promise is fulfilled with. If the promise is rejected, it will throw an
* `Exception` or `Throwable`.
*
* The `coroutine()` function will always return a Promise which will be
* fulfilled with whatever your `$function` returns. Likewise, it will return
* a promise that will be rejected if you throw an `Exception` or `Throwable`
* from your `$function`. This allows you to easily create Promise-based
* functions:
*
* ```php
* $promise = React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $bytes = 0;
* foreach ($urls as $url) {
* $response = yield $browser->get($url);
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* });
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The previous example uses a `yield` statement inside a loop to highlight how
* this vastly simplifies consuming asynchronous operations. At the same time,
* this naive example does not leverage concurrent execution, as it will
* essentially "await" between each operation. In order to take advantage of
* concurrent execution within the given `$function`, you can "await" multiple
* promises by using a single `yield` together with Promise-based primitives
* like this:
*
* ```php
* $promise = React\Async\coroutine(function () {
* $browser = new React\Http\Browser();
* $urls = [
* 'https://example.com/alice',
* 'https://example.com/bob'
* ];
*
* $promises = [];
* foreach ($urls as $url) {
* $promises[] = $browser->get($url);
* }
*
* try {
* $responses = yield React\Promise\all($promises);
* } catch (Exception $e) {
* foreach ($promises as $promise) {
* $promise->cancel();
* }
* throw $e;
* }
*
* $bytes = 0;
* foreach ($responses as $response) {
* assert($response instanceof Psr\Http\Message\ResponseInterface);
* $bytes += $response->getBody()->getSize();
* }
* return $bytes;
* });
*
* $promise->then(function (int $bytes) {
* echo 'Total size: ' . $bytes . PHP_EOL;
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* @template T
* @template TYield
* @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214)
* @template A2
* @template A3
* @template A4
* @template A5
* @param callable(A1, A2, A3, A4, A5):(\Generator<mixed, PromiseInterface<TYield>, TYield, PromiseInterface<T>|T>|PromiseInterface<T>|T) $function
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
* @return PromiseInterface<T>
* @since 3.0.0
*/
function coroutine(callable $function, mixed ...$args): PromiseInterface
{
try {
$generator = $function(...$args);
} catch (\Throwable $e) {
return reject($e);
}
if (!$generator instanceof \Generator) {
return resolve($generator);
}
$promise = null;
/** @var Deferred<T> $deferred*/
$deferred = new Deferred(function () use (&$promise) {
/** @var ?PromiseInterface<T> $promise */
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
$promise = null;
});
/** @var callable $next */
$next = function () use ($deferred, $generator, &$next, &$promise) {
try {
if (!$generator->valid()) {
$next = null;
$deferred->resolve($generator->getReturn());
return;
}
} catch (\Throwable $e) {
$next = null;
$deferred->reject($e);
return;
}
$promise = $generator->current();
if (!$promise instanceof PromiseInterface) {
$next = null;
$deferred->reject(new \UnexpectedValueException(
'Expected coroutine to yield ' . PromiseInterface::class . ', but got ' . (is_object($promise) ? get_class($promise) : gettype($promise))
));
return;
}
/** @var PromiseInterface<TYield> $promise */
assert($next instanceof \Closure);
$promise->then(function ($value) use ($generator, $next) {
$generator->send($value);
$next();
}, function (\Throwable $reason) use ($generator, $next) {
$generator->throw($reason);
$next();
})->then(null, function (\Throwable $reason) use ($deferred, &$next) {
$next = null;
$deferred->reject($reason);
});
};
$next();
return $deferred->promise();
}
/**
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function parallel(iterable $tasks): PromiseInterface
{
/** @var array<int,PromiseInterface<T>> $pending */
$pending = [];
/** @var Deferred<array<T>> $deferred */
$deferred = new Deferred(function () use (&$pending) {
foreach ($pending as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
$pending = [];
});
$results = [];
$continue = true;
$taskErrback = function ($error) use (&$pending, $deferred, &$continue) {
$continue = false;
$deferred->reject($error);
foreach ($pending as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
$pending = [];
};
foreach ($tasks as $i => $task) {
$taskCallback = function ($result) use (&$results, &$pending, &$continue, $i, $deferred) {
$results[$i] = $result;
unset($pending[$i]);
if (!$pending && !$continue) {
$deferred->resolve($results);
}
};
$promise = \call_user_func($task);
assert($promise instanceof PromiseInterface);
$pending[$i] = $promise;
$promise->then($taskCallback, $taskErrback);
if (!$continue) {
break;
}
}
$continue = false;
if (!$pending) {
$deferred->resolve($results);
}
/** @var PromiseInterface<array<T>> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */
return $deferred->promise();
}
/**
* @template T
* @param iterable<callable():(PromiseInterface<T>|T)> $tasks
* @return PromiseInterface<array<T>>
*/
function series(iterable $tasks): PromiseInterface
{
$pending = null;
/** @var Deferred<array<T>> $deferred */
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
$pending = null;
});
$results = [];
if ($tasks instanceof \IteratorAggregate) {
$tasks = $tasks->getIterator();
assert($tasks instanceof \Iterator);
}
$taskCallback = function ($result) use (&$results, &$next) {
$results[] = $result;
/** @var \Closure $next */
$next();
};
$next = function () use (&$tasks, $taskCallback, $deferred, &$results, &$pending) {
if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) {
$deferred->resolve($results);
return;
}
if ($tasks instanceof \Iterator) {
$task = $tasks->current();
$tasks->next();
} else {
assert(\is_array($tasks));
$task = \array_shift($tasks);
}
assert(\is_callable($task));
$promise = \call_user_func($task);
assert($promise instanceof PromiseInterface);
$pending = $promise;
$promise->then($taskCallback, array($deferred, 'reject'));
};
$next();
/** @var PromiseInterface<array<T>> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */
return $deferred->promise();
}
/**
* @template T
* @param iterable<(callable():(PromiseInterface<T>|T))|(callable(mixed):(PromiseInterface<T>|T))> $tasks
* @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)>
*/
function waterfall(iterable $tasks): PromiseInterface
{
$pending = null;
/** @var Deferred<T> $deferred*/
$deferred = new Deferred(function () use (&$pending) {
/** @var ?PromiseInterface<T> $pending */
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
$pending = null;
});
if ($tasks instanceof \IteratorAggregate) {
$tasks = $tasks->getIterator();
assert($tasks instanceof \Iterator);
}
/** @var callable $next */
$next = function ($value = null) use (&$tasks, &$next, $deferred, &$pending) {
if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) {
$deferred->resolve($value);
return;
}
if ($tasks instanceof \Iterator) {
$task = $tasks->current();
$tasks->next();
} else {
assert(\is_array($tasks));
$task = \array_shift($tasks);
}
assert(\is_callable($task));
$promise = \call_user_func_array($task, func_get_args());
assert($promise instanceof PromiseInterface);
$pending = $promise;
$promise->then($next, array($deferred, 'reject'));
};
$next();
return $deferred->promise();
}

View File

@@ -0,0 +1,9 @@
<?php
namespace React\Async;
// @codeCoverageIgnoreStart
if (!\function_exists(__NAMESPACE__ . '\\parallel')) {
require __DIR__ . '/functions.php';
}
// @codeCoverageIgnoreEnd

96
vendor/react/cache/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,96 @@
# Changelog
## 1.2.0 (2022-11-30)
* Feature: Support PHP 8.1 and PHP 8.2.
(#47 by @SimonFrings and #52 by @WyriHaximus)
* Minor documentation improvements.
(#48 by @SimonFrings and #51 by @nhedger)
* Update test suite and use GitHub actions for continuous integration (CI).
(#45 and #49 by @SimonFrings and #54 by @clue)
## 1.1.0 (2020-09-18)
* Feature: Forward compatibility with react/promise 3.
(#39 by @WyriHaximus)
* Add `.gitattributes` to exclude dev files from exports.
(#40 by @reedy)
* Improve test suite, update to support PHP 8 and PHPUnit 9.3.
(#41 and #43 by @SimonFrings and #42 by @WyriHaximus)
## 1.0.0 (2019-07-11)
* 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.
> Contains no other changes, so it's actually fully compatible with the v0.6.0 release.
## 0.6.0 (2019-07-04)
* Feature / BC break: Add support for `getMultiple()`, `setMultiple()`, `deleteMultiple()`, `clear()` and `has()`
supporting multiple cache items (inspired by PSR-16).
(#32 by @krlv and #37 by @clue)
* Documentation for TTL precision with millisecond accuracy or below and
use high-resolution timer for cache TTL on PHP 7.3+.
(#35 and #38 by @clue)
* Improve API documentation and allow legacy HHVM to fail in Travis CI config.
(#34 and #36 by @clue)
* Prefix all global functions calls with \ to skip the look up and resolve process and go straight to the global function.
(#31 by @WyriHaximus)
## 0.5.0 (2018-06-25)
* Improve documentation by describing what is expected of a class implementing `CacheInterface`.
(#21, #22, #23, #27 by @WyriHaximus)
* Implemented (optional) Least Recently Used (LRU) cache algorithm for `ArrayCache`.
(#26 by @clue)
* Added support for cache expiration (TTL).
(#29 by @clue and @WyriHaximus)
* Renamed `remove` to `delete` making it more in line with `PSR-16`.
(#30 by @clue)
## 0.4.2 (2017-12-20)
* Improve documentation with usage and installation instructions
(#10 by @clue)
* Improve test suite by adding PHPUnit to `require-dev` and
add forward compatibility with PHPUnit 5 and PHPUnit 6 and
sanitize Composer autoload paths
(#14 by @shaunbramley and #12 and #18 by @clue)
## 0.4.1 (2016-02-25)
* Repository maintenance, split off from main repo, improve test suite and documentation
* First class support for PHP7 and HHVM (#9 by @clue)
* Adjust compatibility to 5.3 (#7 by @clue)
## 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
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
## 0.3.2 (2013-05-10)
* Version bump
## 0.3.0 (2013-04-14)
* Version bump
## 0.2.6 (2012-12-26)
* Feature: New cache component, used by DNS

21
vendor/react/cache/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.

367
vendor/react/cache/README.md vendored Normal file
View File

@@ -0,0 +1,367 @@
# Cache
[![CI status](https://github.com/reactphp/cache/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/cache/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/cache?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/cache)
Async, [Promise](https://github.com/reactphp/promise)-based cache interface
for [ReactPHP](https://reactphp.org/).
The cache component provides a
[Promise](https://github.com/reactphp/promise)-based
[`CacheInterface`](#cacheinterface) and an in-memory [`ArrayCache`](#arraycache)
implementation of that.
This allows consumers to type hint against the interface and third parties to
provide alternate implementations.
This project is heavily inspired by
[PSR-16: Common Interface for Caching Libraries](https://www.php-fig.org/psr/psr-16/),
but uses an interface more suited for async, non-blocking applications.
**Table of Contents**
* [Usage](#usage)
* [CacheInterface](#cacheinterface)
* [get()](#get)
* [set()](#set)
* [delete()](#delete)
* [getMultiple()](#getmultiple)
* [setMultiple()](#setmultiple)
* [deleteMultiple()](#deletemultiple)
* [clear()](#clear)
* [has()](#has)
* [ArrayCache](#arraycache)
* [Common usage](#common-usage)
* [Fallback get](#fallback-get)
* [Fallback-get-and-set](#fallback-get-and-set)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Usage
### CacheInterface
The `CacheInterface` describes the main interface of this component.
This allows consumers to type hint against the interface and third parties to
provide alternate implementations.
#### get()
The `get(string $key, mixed $default = null): PromiseInterface<mixed>` method can be used to
retrieve an item from the cache.
This method will resolve with the cached value on success or with the
given `$default` value when no item can be found or when an error occurs.
Similarly, an expired cache item (once the time-to-live is expired) is
considered a cache miss.
```php
$cache
->get('foo')
->then('var_dump');
```
This example fetches the value of the key `foo` and passes it to the
`var_dump` function. You can use any of the composition provided by
[promises](https://github.com/reactphp/promise).
#### set()
The `set(string $key, mixed $value, ?float $ttl = null): PromiseInterface<bool>` method can be used to
store an item in the cache.
This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to store
it, it may take a while.
The optional `$ttl` parameter sets the maximum time-to-live in seconds
for this cache item. If this parameter is omitted (or `null`), the item
will stay in the cache for as long as the underlying implementation
supports. Trying to access an expired cache item results in a cache miss,
see also [`get()`](#get).
```php
$cache->set('foo', 'bar', 60);
```
This example eventually sets the value of the key `foo` to `bar`. If it
already exists, it is overridden.
This interface does not enforce any particular TTL resolution, so special
care may have to be taken if you rely on very high precision with
millisecond accuracy or below. Cache implementations SHOULD work on a
best effort basis and SHOULD provide at least second accuracy unless
otherwise noted. Many existing cache implementations are known to provide
microsecond or millisecond accuracy, but it's generally not recommended
to rely on this high precision.
This interface suggests that cache implementations SHOULD use a monotonic
time source if available. Given that a monotonic time source is only
available as of PHP 7.3 by default, cache implementations MAY fall back
to using wall-clock time.
While this does not affect many common use cases, this is an important
distinction for programs that rely on a high time precision or on systems
that are subject to discontinuous time adjustments (time jumps).
This means that if you store a cache item with a TTL of 30s and then
adjust your system time forward by 20s, the cache item SHOULD still
expire in 30s.
#### delete()
The `delete(string $key): PromiseInterface<bool>` method can be used to
delete an item from the cache.
This method will resolve with `true` on success or `false` when an error
occurs. When no item for `$key` is found in the cache, it also resolves
to `true`. If the cache implementation has to go over the network to
delete it, it may take a while.
```php
$cache->delete('foo');
```
This example eventually deletes the key `foo` from the cache. As with
`set()`, this may not happen instantly and a promise is returned to
provide guarantees whether or not the item has been removed from cache.
#### getMultiple()
The `getMultiple(string[] $keys, mixed $default = null): PromiseInterface<array>` method can be used to
retrieve multiple cache items by their unique keys.
This method will resolve with an array of cached values on success or with the
given `$default` value when an item can not be found or when an error occurs.
Similarly, an expired cache item (once the time-to-live is expired) is
considered a cache miss.
```php
$cache->getMultiple(array('name', 'age'))->then(function (array $values) {
$name = $values['name'] ?? 'User';
$age = $values['age'] ?? 'n/a';
echo $name . ' is ' . $age . PHP_EOL;
});
```
This example fetches the cache items for the `name` and `age` keys and
prints some example output. You can use any of the composition provided
by [promises](https://github.com/reactphp/promise).
#### setMultiple()
The `setMultiple(array $values, ?float $ttl = null): PromiseInterface<bool>` method can be used to
persist a set of key => value pairs in the cache, with an optional TTL.
This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to store
it, it may take a while.
The optional `$ttl` parameter sets the maximum time-to-live in seconds
for these cache items. If this parameter is omitted (or `null`), these items
will stay in the cache for as long as the underlying implementation
supports. Trying to access an expired cache items results in a cache miss,
see also [`getMultiple()`](#getmultiple).
```php
$cache->setMultiple(array('foo' => 1, 'bar' => 2), 60);
```
This example eventually sets the list of values - the key `foo` to `1` value
and the key `bar` to `2`. If some of the keys already exist, they are overridden.
#### deleteMultiple()
The `setMultiple(string[] $keys): PromiseInterface<bool>` method can be used to
delete multiple cache items in a single operation.
This method will resolve with `true` on success or `false` when an error
occurs. When no items for `$keys` are found in the cache, it also resolves
to `true`. If the cache implementation has to go over the network to
delete it, it may take a while.
```php
$cache->deleteMultiple(array('foo', 'bar, 'baz'));
```
This example eventually deletes keys `foo`, `bar` and `baz` from the cache.
As with `setMultiple()`, this may not happen instantly and a promise is returned to
provide guarantees whether or not the item has been removed from cache.
#### clear()
The `clear(): PromiseInterface<bool>` method can be used to
wipe clean the entire cache.
This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to
delete it, it may take a while.
```php
$cache->clear();
```
This example eventually deletes all keys from the cache. As with `deleteMultiple()`,
this may not happen instantly and a promise is returned to provide guarantees
whether or not all the items have been removed from cache.
#### has()
The `has(string $key): PromiseInterface<bool>` method can be used to
determine whether an item is present in the cache.
This method will resolve with `true` on success or `false` when no item can be found
or when an error occurs. Similarly, an expired cache item (once the time-to-live
is expired) is considered a cache miss.
```php
$cache
->has('foo')
->then('var_dump');
```
This example checks if the value of the key `foo` is set in the cache and passes
the result to the `var_dump` function. You can use any of the composition provided by
[promises](https://github.com/reactphp/promise).
NOTE: It is recommended that has() is only to be used for cache warming type purposes
and not to be used within your live applications operations for get/set, as this method
is subject to a race condition where your has() will return true and immediately after,
another script can remove it making the state of your app out of date.
### ArrayCache
The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface).
```php
$cache = new ArrayCache();
$cache->set('foo', 'bar');
```
Its constructor accepts an optional `?int $limit` parameter to limit the
maximum number of entries to store in the LRU cache. If you add more
entries to this instance, it will automatically take care of removing
the one that was least recently used (LRU).
For example, this snippet will overwrite the first value and only store
the last two entries:
```php
$cache = new ArrayCache(2);
$cache->set('foo', '1');
$cache->set('bar', '2');
$cache->set('baz', '3');
```
This cache implementation is known to rely on wall-clock time to schedule
future cache expiration times when using any version before PHP 7.3,
because a monotonic time source is only available as of PHP 7.3 (`hrtime()`).
While this does not affect many common use cases, this is an important
distinction for programs that rely on a high time precision or on systems
that are subject to discontinuous time adjustments (time jumps).
This means that if you store a cache item with a TTL of 30s on PHP < 7.3
and then adjust your system time forward by 20s, the cache item may
expire in 10s. See also [`set()`](#set) for more details.
## Common usage
### Fallback get
A common use case of caches is to attempt fetching a cached value and as a
fallback retrieve it from the original data source if not found. Here is an
example of that:
```php
$cache
->get('foo')
->then(function ($result) {
if ($result === null) {
return getFooFromDb();
}
return $result;
})
->then('var_dump');
```
First an attempt is made to retrieve the value of `foo`. A callback function is
registered that will call `getFooFromDb` when the resulting value is null.
`getFooFromDb` is a function (can be any PHP callable) that will be called if the
key does not exist in the cache.
`getFooFromDb` can handle the missing key by returning a promise for the
actual value from the database (or any other data source). As a result, this
chain will correctly fall back, and provide the value in both cases.
### Fallback get and set
To expand on the fallback get example, often you want to set the value on the
cache after fetching it from the data source.
```php
$cache
->get('foo')
->then(function ($result) {
if ($result === null) {
return $this->getAndCacheFooFromDb();
}
return $result;
})
->then('var_dump');
public function getAndCacheFooFromDb()
{
return $this->db
->get('foo')
->then(array($this, 'cacheFooFromDb'));
}
public function cacheFooFromDb($foo)
{
$this->cache->set('foo', $foo);
return $foo;
}
```
By using chaining you can easily conditionally cache the value if it is
fetched from the database.
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
composer require react/cache:^1.2
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use PHP 7+* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
## License
MIT, see [LICENSE file](LICENSE).

45
vendor/react/cache/composer.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
"name": "react/cache",
"description": "Async, Promise-based cache interface for ReactPHP",
"keywords": ["cache", "caching", "promise", "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",
"react/promise": "^3.0 || ^2.0 || ^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35"
},
"autoload": {
"psr-4": {
"React\\Cache\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Cache\\": "tests/"
}
}
}

181
vendor/react/cache/src/ArrayCache.php vendored Normal file
View File

@@ -0,0 +1,181 @@
<?php
namespace React\Cache;
use React\Promise;
use React\Promise\PromiseInterface;
class ArrayCache implements CacheInterface
{
private $limit;
private $data = array();
private $expires = array();
private $supportsHighResolution;
/**
* The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface).
*
* ```php
* $cache = new ArrayCache();
*
* $cache->set('foo', 'bar');
* ```
*
* Its constructor accepts an optional `?int $limit` parameter to limit the
* maximum number of entries to store in the LRU cache. If you add more
* entries to this instance, it will automatically take care of removing
* the one that was least recently used (LRU).
*
* For example, this snippet will overwrite the first value and only store
* the last two entries:
*
* ```php
* $cache = new ArrayCache(2);
*
* $cache->set('foo', '1');
* $cache->set('bar', '2');
* $cache->set('baz', '3');
* ```
*
* This cache implementation is known to rely on wall-clock time to schedule
* future cache expiration times when using any version before PHP 7.3,
* because a monotonic time source is only available as of PHP 7.3 (`hrtime()`).
* While this does not affect many common use cases, this is an important
* distinction for programs that rely on a high time precision or on systems
* that are subject to discontinuous time adjustments (time jumps).
* This means that if you store a cache item with a TTL of 30s on PHP < 7.3
* and then adjust your system time forward by 20s, the cache item may
* expire in 10s. See also [`set()`](#set) for more details.
*
* @param int|null $limit maximum number of entries to store in the LRU cache
*/
public function __construct($limit = null)
{
$this->limit = $limit;
// prefer high-resolution timer, available as of PHP 7.3+
$this->supportsHighResolution = \function_exists('hrtime');
}
public function get($key, $default = null)
{
// delete key if it is already expired => below will detect this as a cache miss
if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) {
unset($this->data[$key], $this->expires[$key]);
}
if (!\array_key_exists($key, $this->data)) {
return Promise\resolve($default);
}
// remove and append to end of array to keep track of LRU info
$value = $this->data[$key];
unset($this->data[$key]);
$this->data[$key] = $value;
return Promise\resolve($value);
}
public function set($key, $value, $ttl = null)
{
// unset before setting to ensure this entry will be added to end of array (LRU info)
unset($this->data[$key]);
$this->data[$key] = $value;
// sort expiration times if TTL is given (first will expire first)
unset($this->expires[$key]);
if ($ttl !== null) {
$this->expires[$key] = $this->now() + $ttl;
\asort($this->expires);
}
// ensure size limit is not exceeded or remove first entry from array
if ($this->limit !== null && \count($this->data) > $this->limit) {
// first try to check if there's any expired entry
// expiration times are sorted, so we can simply look at the first one
\reset($this->expires);
$key = \key($this->expires);
// check to see if the first in the list of expiring keys is already expired
// if the first key is not expired, we have to overwrite by using LRU info
if ($key === null || $this->now() - $this->expires[$key] < 0) {
\reset($this->data);
$key = \key($this->data);
}
unset($this->data[$key], $this->expires[$key]);
}
return Promise\resolve(true);
}
public function delete($key)
{
unset($this->data[$key], $this->expires[$key]);
return Promise\resolve(true);
}
public function getMultiple(array $keys, $default = null)
{
$values = array();
foreach ($keys as $key) {
$values[$key] = $this->get($key, $default);
}
return Promise\all($values);
}
public function setMultiple(array $values, $ttl = null)
{
foreach ($values as $key => $value) {
$this->set($key, $value, $ttl);
}
return Promise\resolve(true);
}
public function deleteMultiple(array $keys)
{
foreach ($keys as $key) {
unset($this->data[$key], $this->expires[$key]);
}
return Promise\resolve(true);
}
public function clear()
{
$this->data = array();
$this->expires = array();
return Promise\resolve(true);
}
public function has($key)
{
// delete key if it is already expired
if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) {
unset($this->data[$key], $this->expires[$key]);
}
if (!\array_key_exists($key, $this->data)) {
return Promise\resolve(false);
}
// remove and append to end of array to keep track of LRU info
$value = $this->data[$key];
unset($this->data[$key]);
$this->data[$key] = $value;
return Promise\resolve(true);
}
/**
* @return float
*/
private function now()
{
return $this->supportsHighResolution ? \hrtime(true) * 1e-9 : \microtime(true);
}
}

View File

@@ -0,0 +1,194 @@
<?php
namespace React\Cache;
use React\Promise\PromiseInterface;
interface CacheInterface
{
/**
* Retrieves an item from the cache.
*
* This method will resolve with the cached value on success or with the
* given `$default` value when no item can be found or when an error occurs.
* Similarly, an expired cache item (once the time-to-live is expired) is
* considered a cache miss.
*
* ```php
* $cache
* ->get('foo')
* ->then('var_dump');
* ```
*
* This example fetches the value of the key `foo` and passes it to the
* `var_dump` function. You can use any of the composition provided by
* [promises](https://github.com/reactphp/promise).
*
* @param string $key
* @param mixed $default Default value to return for cache miss or null if not given.
* @return PromiseInterface<mixed>
*/
public function get($key, $default = null);
/**
* Stores an item in the cache.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. If the cache implementation has to go over the network to store
* it, it may take a while.
*
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
* for this cache item. If this parameter is omitted (or `null`), the item
* will stay in the cache for as long as the underlying implementation
* supports. Trying to access an expired cache item results in a cache miss,
* see also [`get()`](#get).
*
* ```php
* $cache->set('foo', 'bar', 60);
* ```
*
* This example eventually sets the value of the key `foo` to `bar`. If it
* already exists, it is overridden.
*
* This interface does not enforce any particular TTL resolution, so special
* care may have to be taken if you rely on very high precision with
* millisecond accuracy or below. Cache implementations SHOULD work on a
* best effort basis and SHOULD provide at least second accuracy unless
* otherwise noted. Many existing cache implementations are known to provide
* microsecond or millisecond accuracy, but it's generally not recommended
* to rely on this high precision.
*
* This interface suggests that cache implementations SHOULD use a monotonic
* time source if available. Given that a monotonic time source is only
* available as of PHP 7.3 by default, cache implementations MAY fall back
* to using wall-clock time.
* While this does not affect many common use cases, this is an important
* distinction for programs that rely on a high time precision or on systems
* that are subject to discontinuous time adjustments (time jumps).
* This means that if you store a cache item with a TTL of 30s and then
* adjust your system time forward by 20s, the cache item SHOULD still
* expire in 30s.
*
* @param string $key
* @param mixed $value
* @param ?float $ttl
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function set($key, $value, $ttl = null);
/**
* Deletes an item from the cache.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. When no item for `$key` is found in the cache, it also resolves
* to `true`. If the cache implementation has to go over the network to
* delete it, it may take a while.
*
* ```php
* $cache->delete('foo');
* ```
*
* This example eventually deletes the key `foo` from the cache. As with
* `set()`, this may not happen instantly and a promise is returned to
* provide guarantees whether or not the item has been removed from cache.
*
* @param string $key
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function delete($key);
/**
* Retrieves multiple cache items by their unique keys.
*
* This method will resolve with an array of cached values on success or with the
* given `$default` value when an item can not be found or when an error occurs.
* Similarly, an expired cache item (once the time-to-live is expired) is
* considered a cache miss.
*
* ```php
* $cache->getMultiple(array('name', 'age'))->then(function (array $values) {
* $name = $values['name'] ?? 'User';
* $age = $values['age'] ?? 'n/a';
*
* echo $name . ' is ' . $age . PHP_EOL;
* });
* ```
*
* This example fetches the cache items for the `name` and `age` keys and
* prints some example output. You can use any of the composition provided
* by [promises](https://github.com/reactphp/promise).
*
* @param string[] $keys A list of keys that can obtained in a single operation.
* @param mixed $default Default value to return for keys that do not exist.
* @return PromiseInterface<array> Returns a promise which resolves to an `array` of cached values
*/
public function getMultiple(array $keys, $default = null);
/**
* Persists a set of key => value pairs in the cache, with an optional TTL.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. If the cache implementation has to go over the network to store
* it, it may take a while.
*
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
* for these cache items. If this parameter is omitted (or `null`), these items
* will stay in the cache for as long as the underlying implementation
* supports. Trying to access an expired cache items results in a cache miss,
* see also [`get()`](#get).
*
* ```php
* $cache->setMultiple(array('foo' => 1, 'bar' => 2), 60);
* ```
*
* This example eventually sets the list of values - the key `foo` to 1 value
* and the key `bar` to 2. If some of the keys already exist, they are overridden.
*
* @param array $values A list of key => value pairs for a multiple-set operation.
* @param ?float $ttl Optional. The TTL value of this item.
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function setMultiple(array $values, $ttl = null);
/**
* Deletes multiple cache items in a single operation.
*
* @param string[] $keys A list of string-based keys to be deleted.
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function deleteMultiple(array $keys);
/**
* Wipes clean the entire cache.
*
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function clear();
/**
* Determines whether an item is present in the cache.
*
* This method will resolve with `true` on success or `false` when no item can be found
* or when an error occurs. Similarly, an expired cache item (once the time-to-live
* is expired) is considered a cache miss.
*
* ```php
* $cache
* ->has('foo')
* ->then('var_dump');
* ```
*
* This example checks if the value of the key `foo` is set in the cache and passes
* the result to the `var_dump` function. You can use any of the composition provided by
* [promises](https://github.com/reactphp/promise).
*
* NOTE: It is recommended that has() is only to be used for cache warming type purposes
* and not to be used within your live applications operations for get/set, as this method
* is subject to a race condition where your has() will return true and immediately after,
* another script can remove it making the state of your app out of date.
*
* @param string $key The cache item key.
* @return PromiseInterface<bool> Returns a promise which resolves to `true` on success or `false` on error
*/
public function has($key);
}

452
vendor/react/dns/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,452 @@
# Changelog
## 1.13.0 (2024-06-13)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
(#224 by @WyriHaximus)
## 1.12.0 (2023-11-29)
* Feature: Full PHP 8.3 compatibility.
(#217 by @sergiy-petrov)
* Update test environment and avoid unhandled promise rejections.
(#215, #216 and #218 by @clue)
## 1.11.0 (2023-06-02)
* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
(#213 by @clue)
* Improve test suite and project setup and report failed assertions.
(#210 by @clue, #212 by @WyriHaximus and #209 and #211 by @SimonFrings)
## 1.10.0 (2022-09-08)
* Feature: Full support for PHP 8.2 release.
(#201 by @clue and #207 by @WyriHaximus)
* Feature: Optimize forward compatibility with Promise v3, avoid hitting autoloader.
(#202 by @clue)
* Feature / Fix: Improve error reporting when custom error handler is used.
(#197 by @clue)
* Fix: Fix invalid references in exception stack trace.
(#191 by @clue)
* Minor documentation improvements.
(#195 by @SimonFrings and #203 by @nhedger)
* Improve test suite, update to use default loop and new reactphp/async package.
(#204, #205 and #206 by @clue and #196 by @SimonFrings)
## 1.9.0 (2021-12-20)
* Feature: Full support for PHP 8.1 release and prepare PHP 8.2 compatibility
by refactoring `Parser` to avoid assigning dynamic properties.
(#188 and #186 by @clue and #184 by @SimonFrings)
* Feature: Avoid dependency on `ext-filter`.
(#185 by @clue)
* Feature / Fix: Skip invalid nameserver entries from `resolv.conf` and ignore IPv6 zone IDs.
(#187 by @clue)
* Feature / Fix: Reduce socket read chunk size for queries over TCP/IP.
(#189 by @clue)
## 1.8.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).
(#182 by @clue)
```php
// old (still supported)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->create($config, $loop);
// new (using default loop)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->create($config);
```
## 1.7.0 (2021-06-25)
* Feature: Update DNS `Factory` to accept complete `Config` object.
Add new `FallbackExecutor` and use fallback DNS servers when `Config` lists multiple servers.
(#179 and #180 by @clue)
```php
// old (still supported)
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
$resolver = $factory->create($server, $loop);
// new
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$resolver = $factory->create($config, $loop);
```
## 1.6.0 (2021-06-21)
* Feature: Add support for legacy `SPF` record type.
(#178 by @akondas and @clue)
* Fix: Fix integer overflow for TCP/IP chunk size on 32 bit platforms.
(#177 by @clue)
## 1.5.0 (2021-03-05)
* Feature: Improve error reporting when query fails, include domain and query type and DNS server address where applicable.
(#174 by @clue)
* Feature: Improve error handling when sending data to DNS server fails (macOS).
(#171 and #172 by @clue)
* Fix: Improve DNS response parser to limit recursion for compressed labels.
(#169 by @clue)
* Improve test suite, use GitHub actions for continuous integration (CI).
(#170 by @SimonFrings)
## 1.4.0 (2020-09-18)
* Feature: Support upcoming PHP 8.
(#168 by @clue)
* Improve test suite and update to PHPUnit 9.3.
(#164 by @clue, #165 and #166 by @SimonFrings and #167 by @WyriHaximus)
## 1.3.0 (2020-07-10)
* Feature: Forward compatibility with react/promise v3.
(#153 by @WyriHaximus)
* Feature: Support parsing `OPT` records (EDNS0).
(#157 by @clue)
* Fix: Avoid PHP warnings due to lack of args in exception trace on PHP 7.4.
(#160 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite.
(#154 by @reedy, #156 by @clue and #163 by @SimonFrings)
## 1.2.0 (2019-08-15)
* Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection,
add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and
automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`.
(#145, #146, #147 and #148 by @clue)
* Feature: Support escaping literal dots and special characters in domain names.
(#144 by @clue)
## 1.1.0 (2019-07-18)
* Feature: Support parsing `CAA` and `SSHFP` records.
(#141 and #142 by @clue)
* Feature: Add `ResolverInterface` as common interface for `Resolver` class.
(#139 by @clue)
* Fix: Add missing private property definitions and
remove unneeded dependency on `react/stream`.
(#140 and #143 by @clue)
## 1.0.0 (2019-07-11)
* 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 a number of BC breaks due to dropped support for
deprecated functionality and some internal 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 actually not be affected by any BC
breaks, see below for more details:
* BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions
instead of nested arrays and increase code coverage to 100%.
(#130 by @clue)
* BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`,
remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and
add API documentation for `ExecutorInterface`.
(#135, #137 and #138 by @clue)
* BC break: Replace `HeaderBag` attributes with simple `Message` properties.
(#132 by @clue)
* BC break: Mark all `Record` attributes as required, add documentation vs `Query`.
(#136 by @clue)
* BC break: Mark all classes as final to discourage inheritance
(#134 by @WyriHaximus)
## 0.4.19 (2019-07-10)
* Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6.
(#133 by @clue)
## 0.4.18 (2019-09-07)
* Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`,
respect TTL from response records when caching and do not cache truncated responses.
(#129 by @clue)
* Feature: Limit cache size to 256 last responses by default.
(#127 by @clue)
* Feature: Cooperatively resolve hosts to avoid running same query concurrently.
(#125 by @clue)
## 0.4.17 (2019-04-01)
* Feature: Support parsing `authority` and `additional` records from DNS response.
(#123 by @clue)
* Feature: Support dumping records as part of outgoing binary DNS message.
(#124 by @clue)
* Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0
(#121 by @clue)
* Improve test suite to add forward compatibility with PHPUnit 7,
test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM.
(#122 by @clue)
## 0.4.16 (2018-11-11)
* Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references.
(#118 by @clue)
* Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages,
malformed record data or malformed compressed domain name labels.
(#115 and #117 by @clue)
* Fix: Fix interpretation of TTL as UINT32 with most significant bit unset.
(#116 by @clue)
* Fix: Fix caching advanced MX/SRV/TXT/SOA structures.
(#112 by @clue)
## 0.4.15 (2018-07-02)
* Feature: Add `resolveAll()` method to support custom query types in `Resolver`.
(#110 by @clue and @WyriHaximus)
```php
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
```
* Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
(#104, #105, #106, #107 and #108 by @clue)
* Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data.
(#104 by @clue)
* Feature: Improve error messages for failed queries and improve documentation.
(#109 by @clue)
* Feature: Add reverse DNS lookup example.
(#111 by @clue)
## 0.4.14 (2018-06-26)
* Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages
to avoid cache poisoning attacks and deprecate legacy `Executor`.
(#101 and #103 by @clue)
* Feature: Forward compatibility with Cache 0.5
(#102 by @clue)
* Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API.
(#99 by @clue)
## 0.4.13 (2018-02-27)
* Add `Config::loadSystemConfigBlocking()` to load default system config
and support parsing DNS config on all supported platforms
(`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows)
(#92, #93, #94 and #95 by @clue)
```php
$config = Config::loadSystemConfigBlocking();
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
```
* Remove unneeded cyclic dependency on react/socket
(#96 by @clue)
## 0.4.12 (2018-01-14)
* Improve test suite by adding forward compatibility with PHPUnit 6,
test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases,
add test group to skip integration tests relying on internet connection
and add minor documentation improvements.
(#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor)
## 0.4.11 (2017-08-25)
* Feature: Support resolving from default hosts file
(#75, #76 and #77 by @clue)
This means that resolving hosts such as `localhost` will now work as
expected across all platforms with no changes required:
```php
$resolver->resolve('localhost')->then(function ($ip) {
echo 'IP: ' . $ip;
});
```
The new `HostsExecutor` exists for advanced usage and is otherwise used
internally for this feature.
## 0.4.10 (2017-08-10)
* Feature: Forward compatibility with EventLoop v1.0 and v0.5 and
lock minimum dependencies and work around circular dependency for tests
(#70 and #71 by @clue)
* Fix: Work around DNS timeout issues for Windows users
(#74 by @clue)
* Documentation and examples for advanced usage
(#66 by @WyriHaximus)
* Remove broken TCP code, do not retry with invalid TCP query
(#73 by @clue)
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and
lock Travis distro so new defaults will not break the build and
fix failing tests for PHP 7.1
(#68 by @WyriHaximus and #69 and #72 by @clue)
## 0.4.9 (2017-05-01)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#61 by @clue)
## 0.4.8 (2017-04-16)
* Feature: Add support for the AAAA record type to the protocol parser
(#58 by @othillo)
* Feature: Add support for the PTR record type to the protocol parser
(#59 by @othillo)
## 0.4.7 (2017-03-31)
* Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component
(#57 by @clue)
## 0.4.6 (2017-03-11)
* Fix: Fix DNS timeout issues for Windows users and add forward compatibility
with Stream v0.5 and upcoming v0.6
(#53 by @clue)
* Improve test suite by adding PHPUnit to `require-dev`
(#54 by @clue)
## 0.4.5 (2017-03-02)
* Fix: Ensure we ignore the case of the answer
(#51 by @WyriHaximus)
* Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal
code re-use for upcoming versions.
(#48 and #49 by @clue)
## 0.4.4 (2017-02-13)
* Fix: Fix handling connection and stream errors
(#45 by @clue)
* Feature: Add examples and forward compatibility with upcoming Socket v0.5 component
(#46 and #47 by @clue)
## 0.4.3 (2016-07-31)
* Feature: Allow for cache adapter injection (#38 by @WyriHaximus)
```php
$factory = new React\Dns\Resolver\Factory();
$cache = new MyCustomCacheInstance();
$resolver = $factory->createCached('8.8.8.8', $loop, $cache);
```
* Feature: Support Promise cancellation (#35 by @clue)
```php
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();
```
## 0.4.2 (2016-02-24)
* Repository maintenance, split off from main repo, improve test suite and documentation
* First class support for PHP7 and HHVM (#34 by @clue)
* Adjust compatibility to 5.3 (#30 by @clue)
## 0.4.1 (2014-04-13)
* Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus)
## 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
* Bug fix: Properly resolve CNAME aliases
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
* Bump React dependencies to v0.4
## 0.3.2 (2013-05-10)
* Feature: Support default port for IPv6 addresses (@clue)
## 0.3.0 (2013-04-14)
* Bump React dependencies to v0.3
## 0.2.6 (2012-12-26)
* Feature: New cache component, used by DNS
## 0.2.5 (2012-11-26)
* Version bump
## 0.2.4 (2012-11-18)
* Feature: Change to promise-based API (@jsor)
## 0.2.3 (2012-11-14)
* Version bump
## 0.2.2 (2012-10-28)
* Feature: DNS executor timeout handling (@arnaud-lb)
* Feature: DNS retry executor (@arnaud-lb)
## 0.2.1 (2012-10-14)
* Minor adjustments to DNS parser
## 0.2.0 (2012-09-10)
* Feature: DNS resolver

21
vendor/react/dns/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.

453
vendor/react/dns/README.md vendored Normal file
View File

@@ -0,0 +1,453 @@
# DNS
[![CI status](https://github.com/reactphp/dns/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/dns/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/dns?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/dns)
Async DNS resolver for [ReactPHP](https://reactphp.org/).
The main point of the DNS component is to provide async DNS resolution.
However, it is really a toolkit for working with DNS messages, and could
easily be used to create a DNS server.
**Table of contents**
* [Basic usage](#basic-usage)
* [Caching](#caching)
* [Custom cache adapter](#custom-cache-adapter)
* [ResolverInterface](#resolverinterface)
* [resolve()](#resolve)
* [resolveAll()](#resolveall)
* [Advanced usage](#advanced-usage)
* [UdpTransportExecutor](#udptransportexecutor)
* [TcpTransportExecutor](#tcptransportexecutor)
* [SelectiveTransportExecutor](#selectivetransportexecutor)
* [HostsFileExecutor](#hostsfileexecutor)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [References](#references)
## Basic usage
The most basic usage is to just create a resolver through the resolver
factory. All you need to give it is a nameserver, then you can start resolving
names, baby!
```php
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->create($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
```
See also the [first example](examples).
The `Config` class can be used to load the system default config. This is an
operation that may access the filesystem and block. Ideally, this method should
thus be executed only once before the loop starts and not repeatedly while it is
running.
Note that this class may return an *empty* configuration if the system config
can not be loaded. As such, you'll likely want to apply a default nameserver
as above if none can be found.
> Note that the factory loads the hosts file from the filesystem once when
creating the resolver instance.
Ideally, this method should thus be executed only once before the loop starts
and not repeatedly while it is running.
But there's more.
## Caching
You can cache results by configuring the resolver to use a `CachedExecutor`:
```php
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
...
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
```
If the first call returns before the second, only one query will be executed.
The second result will be served from an in memory cache.
This is particularly useful for long running scripts where the same hostnames
have to be looked up multiple times.
See also the [third example](examples).
### Custom cache adapter
By default, the above will use an in memory cache.
You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead:
```php
$cache = new React\Cache\ArrayCache();
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached('8.8.8.8', null, $cache);
```
See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
## ResolverInterface
<a id="resolver"><!-- legacy reference --></a>
### resolve()
The `resolve(string $domain): PromiseInterface<string>` method can be used to
resolve the given $domain name to a single IPv4 address (type `A` query).
```php
$resolver->resolve('reactphp.org')->then(function ($ip) {
echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
});
```
This is one of the main methods in this package. It sends a DNS query
for the given $domain name to your DNS server and returns a single IP
address on success.
If the DNS server sends a DNS response message that contains more than
one IP address for this query, it will randomly pick one of the IP
addresses from the response. If you want the full list of IP addresses
or want to send a different type of query, you should use the
[`resolveAll()`](#resolveall) method instead.
If the DNS server sends a DNS response message that indicates an error
code, this method will reject with a `RecordNotFoundException`. Its
message and code can be used to check for the response code.
If the DNS communication fails and the server does not respond with a
valid response message, this message will reject with an `Exception`.
Pending DNS queries can be cancelled by cancelling its pending promise like so:
```php
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();
```
### resolveAll()
The `resolveAll(string $host, int $type): PromiseInterface<array>` method can be used to
resolve all record values for the given $domain name and query $type.
```php
$resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
```
This is one of the main methods in this package. It sends a DNS query
for the given $domain name to your DNS server and returns a list with all
record values on success.
If the DNS server sends a DNS response message that contains one or more
records for this query, it will return a list with all record values
from the response. You can use the `Message::TYPE_*` constants to control
which type of query will be sent. Note that this method always returns a
list of record values, but each record value type depends on the query
type. For example, it returns the IPv4 addresses for type `A` queries,
the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
`CNAME` and `PTR` queries and structured data for other queries. See also
the `Record` documentation for more details.
If the DNS server sends a DNS response message that indicates an error
code, this method will reject with a `RecordNotFoundException`. Its
message and code can be used to check for the response code.
If the DNS communication fails and the server does not respond with a
valid response message, this message will reject with an `Exception`.
Pending DNS queries can be cancelled by cancelling its pending promise like so:
```php
$promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
$promise->cancel();
```
## Advanced Usage
### UdpTransportExecutor
The `UdpTransportExecutor` can be used to
send DNS queries over a UDP transport.
This is the main class that sends a DNS query to your DNS server and is used
internally by the `Resolver` for the actual message transport.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `igor.io`.
```php
$executor = new UdpTransportExecutor('8.8.8.8:53');
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
See also the [fourth example](examples).
Note that this executor does not implement a timeout, so you will very likely
want to use this in combination with a `TimeoutExecutor` like this:
```php
$executor = new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
);
```
Also note that this executor uses an unreliable UDP transport and that it
does not implement any retry logic, so you will likely want to use this in
combination with a `RetryExecutor` like this:
```php
$executor = new RetryExecutor(
new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
)
);
```
Note that this executor is entirely async and as such allows you to execute
any number of queries concurrently. You should probably limit the number of
concurrent queries in your application or you're very likely going to face
rate limitations and bans on the resolver end. For many common applications,
you may want to avoid sending the same query multiple times when the first
one is still pending, so you will likely want to use this in combination with
a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new RetryExecutor(
new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
)
)
);
```
> Internally, this class uses PHP's UDP sockets and does not take advantage
of [react/datagram](https://github.com/reactphp/datagram) purely for
organizational reasons to avoid a cyclic dependency between the two
packages. Higher-level components should take advantage of the Datagram
component instead of reimplementing this socket logic from scratch.
### TcpTransportExecutor
The `TcpTransportExecutor` class can be used to
send DNS queries over a TCP/IP stream transport.
This is one of the main classes that send a DNS query to your DNS server.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `reactphp.org`.
```php
$executor = new TcpTransportExecutor('8.8.8.8:53');
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
See also [example #92](examples).
Note that this executor does not implement a timeout, so you will very likely
want to use this in combination with a `TimeoutExecutor` like this:
```php
$executor = new TimeoutExecutor(
new TcpTransportExecutor($nameserver),
3.0
);
```
Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
transport, so you do not necessarily have to implement any retry logic.
Note that this executor is entirely async and as such allows you to execute
queries concurrently. The first query will establish a TCP/IP socket
connection to the DNS server which will be kept open for a short period.
Additional queries will automatically reuse this existing socket connection
to the DNS server, will pipeline multiple requests over this single
connection and will keep an idle connection open for a short period. The
initial TCP/IP connection overhead may incur a slight delay if you only send
occasional queries when sending a larger number of concurrent queries over
an existing connection, it becomes increasingly more efficient and avoids
creating many concurrent sockets like the UDP-based executor. You may still
want to limit the number of (concurrent) queries in your application or you
may be facing rate limitations and bans on the resolver end. For many common
applications, you may want to avoid sending the same query multiple times
when the first one is still pending, so you will likely want to use this in
combination with a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new TimeoutExecutor(
new TcpTransportExecutor($nameserver),
3.0
)
);
```
> Internally, this class uses PHP's TCP/IP sockets and does not take advantage
of [react/socket](https://github.com/reactphp/socket) purely for
organizational reasons to avoid a cyclic dependency between the two
packages. Higher-level components should take advantage of the Socket
component instead of reimplementing this socket logic from scratch.
### SelectiveTransportExecutor
The `SelectiveTransportExecutor` class can be used to
Send DNS queries over a UDP or TCP/IP stream transport.
This class will automatically choose the correct transport protocol to send
a DNS query to your DNS server. It will always try to send it over the more
efficient UDP transport first. If this query yields a size related issue
(truncated messages), it will retry over a streaming TCP/IP transport.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `reactphp.org`.
```php
$executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
Note that this executor only implements the logic to select the correct
transport for the given DNS query. Implementing the correct transport logic,
implementing timeouts and any retry logic is left up to the given executors,
see also [`UdpTransportExecutor`](#udptransportexecutor) and
[`TcpTransportExecutor`](#tcptransportexecutor) for more details.
Note that this executor is entirely async and as such allows you to execute
any number of queries concurrently. You should probably limit the number of
concurrent queries in your application or you're very likely going to face
rate limitations and bans on the resolver end. For many common applications,
you may want to avoid sending the same query multiple times when the first
one is still pending, so you will likely want to use this in combination with
a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new SelectiveTransportExecutor(
$datagramExecutor,
$streamExecutor
)
);
```
### HostsFileExecutor
Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
If you also want to take entries from your hosts file into account, you may
use this code:
```php
$hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
$executor = new UdpTransportExecutor('8.8.8.8:53');
$executor = new HostsFileExecutor($hosts, $executor);
$executor->query(
new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
);
```
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
composer require react/dns:^1.13
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
The test suite also contains a number of functional integration tests that rely
on a stable internet connection.
If you do not want to run these, they can simply be skipped like this:
```bash
vendor/bin/phpunit --exclude-group internet
```
## License
MIT, see [LICENSE file](LICENSE).
## References
* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification

49
vendor/react/dns/composer.json vendored Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "react/dns",
"description": "Async DNS resolver for ReactPHP",
"keywords": ["dns", "dns-resolver", "ReactPHP", "async"],
"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",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Dns\\": "tests/"
}
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns;
final class BadServerException extends \Exception
{
}

137
vendor/react/dns/src/Config/Config.php vendored Normal file
View File

@@ -0,0 +1,137 @@
<?php
namespace React\Dns\Config;
use RuntimeException;
final class Config
{
/**
* Loads the system DNS configuration
*
* Note that this method may block while loading its internal files and/or
* commands and should thus be used with care! While this should be
* relatively fast for most systems, it remains unknown if this may block
* under certain circumstances. In particular, this method should only be
* executed before the loop starts, not while it is running.
*
* Note that this method will try to access its files and/or commands and
* try to parse its output. Currently, this will only parse valid nameserver
* entries from its output and will ignore all other output without
* complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @return self
* @codeCoverageIgnore
*/
public static function loadSystemConfigBlocking()
{
// Use WMIC output on Windows
if (DIRECTORY_SEPARATOR === '\\') {
return self::loadWmicBlocking();
}
// otherwise (try to) load from resolv.conf
try {
return self::loadResolvConfBlocking();
} catch (RuntimeException $ignored) {
// return empty config if parsing fails (file not found)
return new self();
}
}
/**
* Loads a resolv.conf file (from the given path or default location)
*
* Note that this method blocks while loading the given path and should
* thus be used with care! While this should be relatively fast for normal
* resolv.conf files, this may be an issue if this file is located on a slow
* device or contains an excessive number of entries. In particular, this
* method should only be executed before the loop starts, not while it is
* running.
*
* Note that this method will throw if the given file can not be loaded,
* such as if it is not readable or does not exist. In particular, this file
* is not available on Windows.
*
* Currently, this will only parse valid "nameserver X" lines from the
* given file contents. Lines can be commented out with "#" and ";" and
* invalid lines will be ignored without complaining. See also
* `man resolv.conf` for more details.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid "nameserver X" lines can be found. See also
* `man resolv.conf` which suggests that the DNS server on the localhost
* should be used in this case. This is left up to higher level consumers
* of this API.
*
* @param ?string $path (optional) path to resolv.conf file or null=load default location
* @return self
* @throws RuntimeException if the path can not be loaded (does not exist)
*/
public static function loadResolvConfBlocking($path = null)
{
if ($path === null) {
$path = '/etc/resolv.conf';
}
$contents = @file_get_contents($path);
if ($contents === false) {
throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"');
}
$matches = array();
preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches);
$config = new self();
foreach ($matches[1] as $ip) {
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
$ip = substr($ip, 0, $pos);
}
if (@inet_pton($ip) !== false) {
$config->nameservers[] = $ip;
}
}
return $config;
}
/**
* Loads the DNS configurations from Windows's WMIC (from the given command or default command)
*
* Note that this method blocks while loading the given command and should
* thus be used with care! While this should be relatively fast for normal
* WMIC commands, it remains unknown if this may block under certain
* circumstances. In particular, this method should only be executed before
* the loop starts, not while it is running.
*
* Note that this method will only try to execute the given command try to
* parse its output, irrespective of whether this command exists. In
* particular, this command is only available on Windows. Currently, this
* will only parse valid nameserver entries from the command output and will
* ignore all other output without complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
* @return self
* @link https://ss64.com/nt/wmic.html
*/
public static function loadWmicBlocking($command = null)
{
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
$config = new self();
$config->nameservers = $matches[1];
return $config;
}
public $nameservers = array();
}

View File

@@ -0,0 +1,153 @@
<?php
namespace React\Dns\Config;
use RuntimeException;
/**
* Represents a static hosts file which maps hostnames to IPs
*
* Hosts files are used on most systems to avoid actually hitting the DNS for
* certain common hostnames.
*
* Most notably, this file usually contains an entry to map "localhost" to the
* local IP. Windows is a notable exception here, as Windows does not actually
* include "localhost" in this file by default. To compensate for this, this
* class may explicitly be wrapped in another HostsFile instance which
* hard-codes these entries for Windows (see also Factory).
*
* This class mostly exists to abstract the parsing/extraction process so this
* can be replaced with a faster alternative in the future.
*/
class HostsFile
{
/**
* Returns the default path for the hosts file on this system
*
* @return string
* @codeCoverageIgnore
*/
public static function getDefaultPath()
{
// use static path for all Unix-based systems
if (DIRECTORY_SEPARATOR !== '\\') {
return '/etc/hosts';
}
// Windows actually stores the path in the registry under
// \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath
$path = '%SystemRoot%\\system32\drivers\etc\hosts';
$base = getenv('SystemRoot');
if ($base === false) {
$base = 'C:\\Windows';
}
return str_replace('%SystemRoot%', $base, $path);
}
/**
* Loads a hosts file (from the given path or default location)
*
* Note that this method blocks while loading the given path and should
* thus be used with care! While this should be relatively fast for normal
* hosts file, this may be an issue if this file is located on a slow device
* or contains an excessive number of entries. In particular, this method
* should only be executed before the loop starts, not while it is running.
*
* @param ?string $path (optional) path to hosts file or null=load default location
* @return self
* @throws RuntimeException if the path can not be loaded (does not exist)
*/
public static function loadFromPathBlocking($path = null)
{
if ($path === null) {
$path = self::getDefaultPath();
}
$contents = @file_get_contents($path);
if ($contents === false) {
throw new RuntimeException('Unable to load hosts file "' . $path . '"');
}
return new self($contents);
}
private $contents;
/**
* Instantiate new hosts file with the given hosts file contents
*
* @param string $contents
*/
public function __construct($contents)
{
// remove all comments from the contents
$contents = preg_replace('/[ \t]*#.*/', '', strtolower($contents));
$this->contents = $contents;
}
/**
* Returns all IPs for the given hostname
*
* @param string $name
* @return string[]
*/
public function getIpsForHost($name)
{
$name = strtolower($name);
$ips = array();
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
$parts = preg_split('/\s+/', $line);
$ip = array_shift($parts);
if ($parts && array_search($name, $parts) !== false) {
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
$ip = substr($ip, 0, $pos);
}
if (@inet_pton($ip) !== false) {
$ips[] = $ip;
}
}
}
return $ips;
}
/**
* Returns all hostnames for the given IPv4 or IPv6 address
*
* @param string $ip
* @return string[]
*/
public function getHostsForIp($ip)
{
// check binary representation of IP to avoid string case and short notation
$ip = @inet_pton($ip);
if ($ip === false) {
return array();
}
$names = array();
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
$parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
$addr = (string) array_shift($parts);
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) {
$addr = substr($addr, 0, $pos);
}
if (@inet_pton($addr) === $ip) {
foreach ($parts as $part) {
$names[] = $part;
}
}
}
return $names;
}
}

230
vendor/react/dns/src/Model/Message.php vendored Normal file
View File

@@ -0,0 +1,230 @@
<?php
namespace React\Dns\Model;
use React\Dns\Query\Query;
/**
* This class represents an outgoing query message or an incoming response message
*
* @link https://tools.ietf.org/html/rfc1035#section-4.1.1
*/
final class Message
{
const TYPE_A = 1;
const TYPE_NS = 2;
const TYPE_CNAME = 5;
const TYPE_SOA = 6;
const TYPE_PTR = 12;
const TYPE_MX = 15;
const TYPE_TXT = 16;
const TYPE_AAAA = 28;
const TYPE_SRV = 33;
const TYPE_SSHFP = 44;
/**
* pseudo-type for EDNS0
*
* These are included in the additional section and usually not in answer section.
* Defined in [RFC 6891](https://tools.ietf.org/html/rfc6891) (or older
* [RFC 2671](https://tools.ietf.org/html/rfc2671)).
*
* The OPT record uses the "class" field to store the maximum size.
*
* The OPT record uses the "ttl" field to store additional flags.
*/
const TYPE_OPT = 41;
/**
* Sender Policy Framework (SPF) had a dedicated SPF type which has been
* deprecated in favor of reusing the existing TXT type.
*
* @deprecated https://datatracker.ietf.org/doc/html/rfc7208#section-3.1
* @see self::TYPE_TXT
*/
const TYPE_SPF = 99;
const TYPE_ANY = 255;
const TYPE_CAA = 257;
const CLASS_IN = 1;
const OPCODE_QUERY = 0;
const OPCODE_IQUERY = 1; // inverse query
const OPCODE_STATUS = 2;
const RCODE_OK = 0;
const RCODE_FORMAT_ERROR = 1;
const RCODE_SERVER_FAILURE = 2;
const RCODE_NAME_ERROR = 3;
const RCODE_NOT_IMPLEMENTED = 4;
const RCODE_REFUSED = 5;
/**
* The edns-tcp-keepalive EDNS0 Option
*
* Option value contains a `?float` with timeout in seconds (in 0.1s steps)
* for DNS response or `null` for DNS query.
*
* @link https://tools.ietf.org/html/rfc7828
*/
const OPT_TCP_KEEPALIVE = 11;
/**
* The EDNS(0) Padding Option
*
* Option value contains a `string` with binary data (usually variable
* number of null bytes)
*
* @link https://tools.ietf.org/html/rfc7830
*/
const OPT_PADDING = 12;
/**
* Creates a new request message for the given query
*
* @param Query $query
* @return self
*/
public static function createRequestForQuery(Query $query)
{
$request = new Message();
$request->id = self::generateId();
$request->rd = true;
$request->questions[] = $query;
return $request;
}
/**
* Creates a new response message for the given query with the given answer records
*
* @param Query $query
* @param Record[] $answers
* @return self
*/
public static function createResponseWithAnswersForQuery(Query $query, array $answers)
{
$response = new Message();
$response->id = self::generateId();
$response->qr = true;
$response->rd = true;
$response->questions[] = $query;
foreach ($answers as $record) {
$response->answers[] = $record;
}
return $response;
}
/**
* generates a random 16 bit message ID
*
* This uses a CSPRNG so that an outside attacker that is sending spoofed
* DNS response messages can not guess the message ID to avoid possible
* cache poisoning attacks.
*
* The `random_int()` function is only available on PHP 7+ or when
* https://github.com/paragonie/random_compat is installed. As such, using
* the latest supported PHP version is highly recommended. This currently
* falls back to a less secure random number generator on older PHP versions
* in the hope that this system is properly protected against outside
* attackers, for example by using one of the common local DNS proxy stubs.
*
* @return int
* @see self::getId()
* @codeCoverageIgnore
*/
private static function generateId()
{
if (function_exists('random_int')) {
return random_int(0, 0xffff);
}
return mt_rand(0, 0xffff);
}
/**
* The 16 bit message ID
*
* The response message ID has to match the request message ID. This allows
* the receiver to verify this is the correct response message. An outside
* attacker may try to inject fake responses by "guessing" the message ID,
* so this should use a proper CSPRNG to avoid possible cache poisoning.
*
* @var int 16 bit message ID
* @see self::generateId()
*/
public $id = 0;
/**
* @var bool Query/Response flag, query=false or response=true
*/
public $qr = false;
/**
* @var int specifies the kind of query (4 bit), see self::OPCODE_* constants
* @see self::OPCODE_QUERY
*/
public $opcode = self::OPCODE_QUERY;
/**
*
* @var bool Authoritative Answer
*/
public $aa = false;
/**
* @var bool TrunCation
*/
public $tc = false;
/**
* @var bool Recursion Desired
*/
public $rd = false;
/**
* @var bool Recursion Available
*/
public $ra = false;
/**
* @var int response code (4 bit), see self::RCODE_* constants
* @see self::RCODE_OK
*/
public $rcode = Message::RCODE_OK;
/**
* An array of Query objects
*
* ```php
* $questions = array(
* new Query(
* 'reactphp.org',
* Message::TYPE_A,
* Message::CLASS_IN
* )
* );
* ```
*
* @var Query[]
*/
public $questions = array();
/**
* @var Record[]
*/
public $answers = array();
/**
* @var Record[]
*/
public $authority = array();
/**
* @var Record[]
*/
public $additional = array();
}

153
vendor/react/dns/src/Model/Record.php vendored Normal file
View File

@@ -0,0 +1,153 @@
<?php
namespace React\Dns\Model;
/**
* This class represents a single resulting record in a response message
*
* It uses a structure similar to `\React\Dns\Query\Query`, but does include
* fields for resulting TTL and resulting record data (IPs etc.).
*
* @link https://tools.ietf.org/html/rfc1035#section-4.1.3
* @see \React\Dns\Query\Query
*/
final class Record
{
/**
* @var string hostname without trailing dot, for example "reactphp.org"
*/
public $name;
/**
* @var int see Message::TYPE_* constants (UINT16)
*/
public $type;
/**
* Defines the network class, usually `Message::CLASS_IN`.
*
* For `OPT` records (EDNS0), this defines the maximum message size instead.
*
* @var int see Message::CLASS_IN constant (UINT16)
* @see Message::CLASS_IN
*/
public $class;
/**
* Defines the maximum time-to-live (TTL) in seconds
*
* For `OPT` records (EDNS0), this defines additional flags instead.
*
* @var int maximum TTL in seconds (UINT32, most significant bit always unset)
* @link https://tools.ietf.org/html/rfc2181#section-8
* @link https://tools.ietf.org/html/rfc6891#section-6.1.3 for `OPT` records (EDNS0)
*/
public $ttl;
/**
* The payload data for this record
*
* The payload data format depends on the record type. As a rule of thumb,
* this library will try to express this in a way that can be consumed
* easily without having to worry about DNS internals and its binary transport:
*
* - A:
* IPv4 address string, for example "192.168.1.1".
*
* - AAAA:
* IPv6 address string, for example "::1".
*
* - CNAME / PTR / NS:
* The hostname without trailing dot, for example "reactphp.org".
*
* - TXT:
* List of string values, for example `["v=spf1 include:example.com"]`.
* This is commonly a list with only a single string value, but this
* technically allows multiple strings (0-255 bytes each) in a single
* record. This is rarely used and depending on application you may want
* to join these together or handle them separately. Each string can
* transport any binary data, its character encoding is not defined (often
* ASCII/UTF-8 in practice). [RFC 1464](https://tools.ietf.org/html/rfc1464)
* suggests using key-value pairs such as `["name=test","version=1"]`, but
* interpretation of this is not enforced and left up to consumers of this
* library (used for DNS-SD/Zeroconf and others).
*
* - MX:
* Mail server priority (UINT16) and target hostname without trailing dot,
* for example `{"priority":10,"target":"mx.example.com"}`.
* The payload data uses an associative array with fixed keys "priority"
* (also commonly referred to as weight or preference) and "target" (also
* referred to as exchange). If a response message contains multiple
* records of this type, targets should be sorted by priority (lowest
* first) - this is left up to consumers of this library (used for SMTP).
*
* - SRV:
* Service priority (UINT16), service weight (UINT16), service port (UINT16)
* and target hostname without trailing dot, for example
* `{"priority":10,"weight":50,"port":8080,"target":"example.com"}`.
* The payload data uses an associative array with fixed keys "priority",
* "weight", "port" and "target" (also referred to as name).
* The target may be an empty host name string if the service is decidedly
* not available. If a response message contains multiple records of this
* type, targets should be sorted by priority (lowest first) and selected
* randomly according to their weight - this is left up to consumers of
* this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782)
* for more details.
*
* - SSHFP:
* Includes algorithm (UNIT8), fingerprint type (UNIT8) and fingerprint
* value as lower case hex string, for example:
* `{"algorithm":1,"type":1,"fingerprint":"0123456789abcdef..."}`
* See also https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
* for algorithm and fingerprint type assignments.
*
* - SOA:
* Includes master hostname without trailing dot, responsible person email
* as hostname without trailing dot and serial, refresh, retry, expire and
* minimum times in seconds (UINT32 each), for example:
* `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial":
* 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`.
*
* - CAA:
* Includes flag (UNIT8), tag string and value string, for example:
* `{"flag":128,"tag":"issue","value":"letsencrypt.org"}`
*
* - OPT:
* Special pseudo-type for EDNS0. Includes an array of additional opt codes
* with a value according to the respective OPT code. See `Message::OPT_*`
* for list of supported OPT codes. Any other OPT code not currently
* supported will be an opaque binary string containing the raw data
* as transported in the DNS record. For forwards compatibility, you should
* not rely on this format for unknown types. Future versions may add
* support for new types and this may then parse the payload data
* appropriately - this will not be considered a BC break. See also
* [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details.
*
* - Any other unknown type:
* An opaque binary string containing the RDATA as transported in the DNS
* record. For forwards compatibility, you should not rely on this format
* for unknown types. Future versions may add support for new types and
* this may then parse the payload data appropriately - this will not be
* considered a BC break. See the format definition of known types above
* for more details.
*
* @var string|string[]|array
*/
public $data;
/**
* @param string $name
* @param int $type
* @param int $class
* @param int $ttl
* @param string|string[]|array $data
*/
public function __construct($name, $type, $class, $ttl, $data)
{
$this->name = $name;
$this->type = $type;
$this->class = $class;
$this->ttl = $ttl;
$this->data = $data;
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace React\Dns\Protocol;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Dns\Query\Query;
final class BinaryDumper
{
/**
* @param Message $message
* @return string
*/
public function toBinary(Message $message)
{
$data = '';
$data .= $this->headerToBinary($message);
$data .= $this->questionToBinary($message->questions);
$data .= $this->recordsToBinary($message->answers);
$data .= $this->recordsToBinary($message->authority);
$data .= $this->recordsToBinary($message->additional);
return $data;
}
/**
* @param Message $message
* @return string
*/
private function headerToBinary(Message $message)
{
$data = '';
$data .= pack('n', $message->id);
$flags = 0x00;
$flags = ($flags << 1) | ($message->qr ? 1 : 0);
$flags = ($flags << 4) | $message->opcode;
$flags = ($flags << 1) | ($message->aa ? 1 : 0);
$flags = ($flags << 1) | ($message->tc ? 1 : 0);
$flags = ($flags << 1) | ($message->rd ? 1 : 0);
$flags = ($flags << 1) | ($message->ra ? 1 : 0);
$flags = ($flags << 3) | 0; // skip unused zero bit
$flags = ($flags << 4) | $message->rcode;
$data .= pack('n', $flags);
$data .= pack('n', count($message->questions));
$data .= pack('n', count($message->answers));
$data .= pack('n', count($message->authority));
$data .= pack('n', count($message->additional));
return $data;
}
/**
* @param Query[] $questions
* @return string
*/
private function questionToBinary(array $questions)
{
$data = '';
foreach ($questions as $question) {
$data .= $this->domainNameToBinary($question->name);
$data .= pack('n*', $question->type, $question->class);
}
return $data;
}
/**
* @param Record[] $records
* @return string
*/
private function recordsToBinary(array $records)
{
$data = '';
foreach ($records as $record) {
/* @var $record Record */
switch ($record->type) {
case Message::TYPE_A:
case Message::TYPE_AAAA:
$binary = \inet_pton($record->data);
break;
case Message::TYPE_CNAME:
case Message::TYPE_NS:
case Message::TYPE_PTR:
$binary = $this->domainNameToBinary($record->data);
break;
case Message::TYPE_TXT:
case Message::TYPE_SPF:
$binary = $this->textsToBinary($record->data);
break;
case Message::TYPE_MX:
$binary = \pack(
'n',
$record->data['priority']
);
$binary .= $this->domainNameToBinary($record->data['target']);
break;
case Message::TYPE_SRV:
$binary = \pack(
'n*',
$record->data['priority'],
$record->data['weight'],
$record->data['port']
);
$binary .= $this->domainNameToBinary($record->data['target']);
break;
case Message::TYPE_SOA:
$binary = $this->domainNameToBinary($record->data['mname']);
$binary .= $this->domainNameToBinary($record->data['rname']);
$binary .= \pack(
'N*',
$record->data['serial'],
$record->data['refresh'],
$record->data['retry'],
$record->data['expire'],
$record->data['minimum']
);
break;
case Message::TYPE_CAA:
$binary = \pack(
'C*',
$record->data['flag'],
\strlen($record->data['tag'])
);
$binary .= $record->data['tag'];
$binary .= $record->data['value'];
break;
case Message::TYPE_SSHFP:
$binary = \pack(
'CCH*',
$record->data['algorithm'],
$record->data['type'],
$record->data['fingerprint']
);
break;
case Message::TYPE_OPT:
$binary = '';
foreach ($record->data as $opt => $value) {
if ($opt === Message::OPT_TCP_KEEPALIVE && $value !== null) {
$value = \pack('n', round($value * 10));
}
$binary .= \pack('n*', $opt, \strlen((string) $value)) . $value;
}
break;
default:
// RDATA is already stored as binary value for unknown record types
$binary = $record->data;
}
$data .= $this->domainNameToBinary($record->name);
$data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary));
$data .= $binary;
}
return $data;
}
/**
* @param string[] $texts
* @return string
*/
private function textsToBinary(array $texts)
{
$data = '';
foreach ($texts as $text) {
$data .= \chr(\strlen($text)) . $text;
}
return $data;
}
/**
* @param string $host
* @return string
*/
private function domainNameToBinary($host)
{
if ($host === '') {
return "\0";
}
// break up domain name at each dot that is not preceeded by a backslash (escaped notation)
return $this->textsToBinary(
\array_map(
'stripcslashes',
\preg_split(
'/(?<!\\\\)\./',
$host . '.'
)
)
);
}
}

356
vendor/react/dns/src/Protocol/Parser.php vendored Normal file
View File

@@ -0,0 +1,356 @@
<?php
namespace React\Dns\Protocol;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Dns\Query\Query;
use InvalidArgumentException;
/**
* DNS protocol parser
*
* Obsolete and uncommon types and classes are not implemented.
*/
final class Parser
{
/**
* Parses the given raw binary message into a Message object
*
* @param string $data
* @throws InvalidArgumentException
* @return Message
*/
public function parseMessage($data)
{
$message = $this->parse($data, 0);
if ($message === null) {
throw new InvalidArgumentException('Unable to parse binary message');
}
return $message;
}
/**
* @param string $data
* @param int $consumed
* @return ?Message
*/
private function parse($data, $consumed)
{
if (!isset($data[12 - 1])) {
return null;
}
list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($data, 0, 12)));
$message = new Message();
$message->id = $id;
$message->rcode = $fields & 0xf;
$message->ra = (($fields >> 7) & 1) === 1;
$message->rd = (($fields >> 8) & 1) === 1;
$message->tc = (($fields >> 9) & 1) === 1;
$message->aa = (($fields >> 10) & 1) === 1;
$message->opcode = ($fields >> 11) & 0xf;
$message->qr = (($fields >> 15) & 1) === 1;
$consumed += 12;
// parse all questions
for ($i = $qdCount; $i > 0; --$i) {
list($question, $consumed) = $this->parseQuestion($data, $consumed);
if ($question === null) {
return null;
} else {
$message->questions[] = $question;
}
}
// parse all answer records
for ($i = $anCount; $i > 0; --$i) {
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return null;
} else {
$message->answers[] = $record;
}
}
// parse all authority records
for ($i = $nsCount; $i > 0; --$i) {
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return null;
} else {
$message->authority[] = $record;
}
}
// parse all additional records
for ($i = $arCount; $i > 0; --$i) {
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return null;
} else {
$message->additional[] = $record;
}
}
return $message;
}
/**
* @param string $data
* @param int $consumed
* @return array
*/
private function parseQuestion($data, $consumed)
{
list($labels, $consumed) = $this->readLabels($data, $consumed);
if ($labels === null || !isset($data[$consumed + 4 - 1])) {
return array(null, null);
}
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
$consumed += 4;
return array(
new Query(
implode('.', $labels),
$type,
$class
),
$consumed
);
}
/**
* @param string $data
* @param int $consumed
* @return array An array with a parsed Record on success or array with null if data is invalid/incomplete
*/
private function parseRecord($data, $consumed)
{
list($name, $consumed) = $this->readDomain($data, $consumed);
if ($name === null || !isset($data[$consumed + 10 - 1])) {
return array(null, null);
}
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
$consumed += 4;
list($ttl) = array_values(unpack('N', substr($data, $consumed, 4)));
$consumed += 4;
// TTL is a UINT32 that must not have most significant bit set for BC reasons
if ($ttl < 0 || $ttl >= 1 << 31) {
$ttl = 0;
}
list($rdLength) = array_values(unpack('n', substr($data, $consumed, 2)));
$consumed += 2;
if (!isset($data[$consumed + $rdLength - 1])) {
return array(null, null);
}
$rdata = null;
$expected = $consumed + $rdLength;
if (Message::TYPE_A === $type) {
if ($rdLength === 4) {
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
$consumed += $rdLength;
}
} elseif (Message::TYPE_AAAA === $type) {
if ($rdLength === 16) {
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
$consumed += $rdLength;
}
} elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
list($rdata, $consumed) = $this->readDomain($data, $consumed);
} elseif (Message::TYPE_TXT === $type || Message::TYPE_SPF === $type) {
$rdata = array();
while ($consumed < $expected) {
$len = ord($data[$consumed]);
$rdata[] = (string)substr($data, $consumed + 1, $len);
$consumed += $len + 1;
}
} elseif (Message::TYPE_MX === $type) {
if ($rdLength > 2) {
list($priority) = array_values(unpack('n', substr($data, $consumed, 2)));
list($target, $consumed) = $this->readDomain($data, $consumed + 2);
$rdata = array(
'priority' => $priority,
'target' => $target
);
}
} elseif (Message::TYPE_SRV === $type) {
if ($rdLength > 6) {
list($priority, $weight, $port) = array_values(unpack('n*', substr($data, $consumed, 6)));
list($target, $consumed) = $this->readDomain($data, $consumed + 6);
$rdata = array(
'priority' => $priority,
'weight' => $weight,
'port' => $port,
'target' => $target
);
}
} elseif (Message::TYPE_SSHFP === $type) {
if ($rdLength > 2) {
list($algorithm, $hash) = \array_values(\unpack('C*', \substr($data, $consumed, 2)));
$fingerprint = \bin2hex(\substr($data, $consumed + 2, $rdLength - 2));
$consumed += $rdLength;
$rdata = array(
'algorithm' => $algorithm,
'type' => $hash,
'fingerprint' => $fingerprint
);
}
} elseif (Message::TYPE_SOA === $type) {
list($mname, $consumed) = $this->readDomain($data, $consumed);
list($rname, $consumed) = $this->readDomain($data, $consumed);
if ($mname !== null && $rname !== null && isset($data[$consumed + 20 - 1])) {
list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($data, $consumed, 20)));
$consumed += 20;
$rdata = array(
'mname' => $mname,
'rname' => $rname,
'serial' => $serial,
'refresh' => $refresh,
'retry' => $retry,
'expire' => $expire,
'minimum' => $minimum
);
}
} elseif (Message::TYPE_OPT === $type) {
$rdata = array();
while (isset($data[$consumed + 4 - 1])) {
list($code, $length) = array_values(unpack('n*', substr($data, $consumed, 4)));
$value = (string) substr($data, $consumed + 4, $length);
if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') {
$value = null;
} elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) {
list($value) = array_values(unpack('n', $value));
$value = round($value * 0.1, 1);
} elseif ($code === Message::OPT_TCP_KEEPALIVE) {
break;
}
$rdata[$code] = $value;
$consumed += 4 + $length;
}
} elseif (Message::TYPE_CAA === $type) {
if ($rdLength > 3) {
list($flag, $tagLength) = array_values(unpack('C*', substr($data, $consumed, 2)));
if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) {
$tag = substr($data, $consumed + 2, $tagLength);
$value = substr($data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
$consumed += $rdLength;
$rdata = array(
'flag' => $flag,
'tag' => $tag,
'value' => $value
);
}
}
} else {
// unknown types simply parse rdata as an opaque binary string
$rdata = substr($data, $consumed, $rdLength);
$consumed += $rdLength;
}
// ensure parsing record data consumes expact number of bytes indicated in record length
if ($consumed !== $expected || $rdata === null) {
return array(null, null);
}
return array(
new Record($name, $type, $class, $ttl, $rdata),
$consumed
);
}
private function readDomain($data, $consumed)
{
list ($labels, $consumed) = $this->readLabels($data, $consumed);
if ($labels === null) {
return array(null, null);
}
// use escaped notation for each label part, then join using dots
return array(
\implode(
'.',
\array_map(
function ($label) {
return \addcslashes($label, "\0..\40.\177");
},
$labels
)
),
$consumed
);
}
/**
* @param string $data
* @param int $consumed
* @param int $compressionDepth maximum depth for compressed labels to avoid unreasonable recursion
* @return array
*/
private function readLabels($data, $consumed, $compressionDepth = 127)
{
$labels = array();
while (true) {
if (!isset($data[$consumed])) {
return array(null, null);
}
$length = \ord($data[$consumed]);
// end of labels reached
if ($length === 0) {
$consumed += 1;
break;
}
// first two bits set? this is a compressed label (14 bit pointer offset)
if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1]) && $compressionDepth) {
$offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]);
if ($offset >= $consumed) {
return array(null, null);
}
$consumed += 2;
list($newLabels) = $this->readLabels($data, $offset, $compressionDepth - 1);
if ($newLabels === null) {
return array(null, null);
}
$labels = array_merge($labels, $newLabels);
break;
}
// length MUST be 0-63 (6 bits only) and data has to be large enough
if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) {
return array(null, null);
}
$labels[] = substr($data, $consumed + 1, $length);
$consumed += $length + 1;
}
return array($labels, $consumed);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace React\Dns\Query;
use React\Cache\CacheInterface;
use React\Dns\Model\Message;
use React\Promise\Promise;
final class CachingExecutor implements ExecutorInterface
{
/**
* Default TTL for negative responses (NXDOMAIN etc.).
*
* @internal
*/
const TTL = 60;
private $executor;
private $cache;
public function __construct(ExecutorInterface $executor, CacheInterface $cache)
{
$this->executor = $executor;
$this->cache = $cache;
}
public function query(Query $query)
{
$id = $query->name . ':' . $query->type . ':' . $query->class;
$cache = $this->cache;
$that = $this;
$executor = $this->executor;
$pending = $cache->get($id);
return new Promise(function ($resolve, $reject) use ($query, $id, $cache, $executor, &$pending, $that) {
$pending->then(
function ($message) use ($query, $id, $cache, $executor, &$pending, $that) {
// return cached response message on cache hit
if ($message !== null) {
return $message;
}
// perform DNS lookup if not already cached
return $pending = $executor->query($query)->then(
function (Message $message) use ($cache, $id, $that) {
// DNS response message received => store in cache when not truncated and return
if (!$message->tc) {
$cache->set($id, $message, $that->ttl($message));
}
return $message;
}
);
}
)->then($resolve, function ($e) use ($reject, &$pending) {
$reject($e);
$pending = null;
});
}, function ($_, $reject) use (&$pending, $query) {
$reject(new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled'));
$pending->cancel();
$pending = null;
});
}
/**
* @param Message $message
* @return int
* @internal
*/
public function ttl(Message $message)
{
// select TTL from answers (should all be the same), use smallest value if available
// @link https://tools.ietf.org/html/rfc2181#section-5.2
$ttl = null;
foreach ($message->answers as $answer) {
if ($ttl === null || $answer->ttl < $ttl) {
$ttl = $answer->ttl;
}
}
if ($ttl === null) {
$ttl = self::TTL;
}
return $ttl;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns\Query;
final class CancellationException extends \RuntimeException
{
}

View File

@@ -0,0 +1,91 @@
<?php
namespace React\Dns\Query;
use React\Promise\Promise;
/**
* Cooperatively resolves hosts via the given base executor to ensure same query is not run concurrently
*
* Wraps an existing `ExecutorInterface` to keep tracking of pending queries
* and only starts a new query when the same query is not already pending. Once
* the underlying query is fulfilled/rejected, it will forward its value to all
* promises awaiting the same query.
*
* This means it will not limit concurrency for queries that differ, for example
* when sending many queries for different host names or types.
*
* This is useful because all executors are entirely async and as such allow you
* to execute any number of queries concurrently. You should probably limit the
* number of concurrent queries in your application or you're very likely going
* to face rate limitations and bans on the resolver end. For many common
* applications, you may want to avoid sending the same query multiple times
* when the first one is still pending, so you will likely want to use this in
* combination with some other executor like this:
*
* ```php
* $executor = new CoopExecutor(
* new RetryExecutor(
* new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* )
* )
* );
* ```
*/
final class CoopExecutor implements ExecutorInterface
{
private $executor;
private $pending = array();
private $counts = array();
public function __construct(ExecutorInterface $base)
{
$this->executor = $base;
}
public function query(Query $query)
{
$key = $this->serializeQueryToIdentity($query);
if (isset($this->pending[$key])) {
// same query is already pending, so use shared reference to pending query
$promise = $this->pending[$key];
++$this->counts[$key];
} else {
// no such query pending, so start new query and keep reference until it's fulfilled or rejected
$promise = $this->executor->query($query);
$this->pending[$key] = $promise;
$this->counts[$key] = 1;
$pending =& $this->pending;
$counts =& $this->counts;
$promise->then(function () use ($key, &$pending, &$counts) {
unset($pending[$key], $counts[$key]);
}, function () use ($key, &$pending, &$counts) {
unset($pending[$key], $counts[$key]);
});
}
// Return a child promise awaiting the pending query.
// Cancelling this child promise should only cancel the pending query
// when no other child promise is awaiting the same query.
$pending =& $this->pending;
$counts =& $this->counts;
return new Promise(function ($resolve, $reject) use ($promise) {
$promise->then($resolve, $reject);
}, function () use (&$promise, $key, $query, &$pending, &$counts) {
if (--$counts[$key] < 1) {
unset($pending[$key], $counts[$key]);
$promise->cancel();
$promise = null;
}
throw new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled');
});
}
private function serializeQueryToIdentity(Query $query)
{
return sprintf('%s:%s:%s', $query->name, $query->type, $query->class);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace React\Dns\Query;
interface ExecutorInterface
{
/**
* Executes a query and will return a response message
*
* It returns a Promise which either fulfills with a response
* `React\Dns\Model\Message` on success or rejects with an `Exception` if
* the query is not successful. A response message may indicate an error
* condition in its `rcode`, but this is considered a valid response message.
*
* ```php
* $executor->query($query)->then(
* function (React\Dns\Model\Message $response) {
* // response message successfully received
* var_dump($response->rcode, $response->answers);
* },
* function (Exception $error) {
* // failed to query due to $error
* }
* );
* ```
*
* The returned Promise MUST be implemented in such a way that it can be
* cancelled when it is still pending. Cancelling a pending promise MUST
* reject its value with an Exception. It SHOULD clean up any underlying
* resources and references as applicable.
*
* ```php
* $promise = $executor->query($query);
*
* $promise->cancel();
* ```
*
* @param Query $query
* @return \React\Promise\PromiseInterface<\React\Dns\Model\Message>
* resolves with response message on success or rejects with an Exception on error
*/
public function query(Query $query);
}

View File

@@ -0,0 +1,49 @@
<?php
namespace React\Dns\Query;
use React\Promise\Promise;
final class FallbackExecutor implements ExecutorInterface
{
private $executor;
private $fallback;
public function __construct(ExecutorInterface $executor, ExecutorInterface $fallback)
{
$this->executor = $executor;
$this->fallback = $fallback;
}
public function query(Query $query)
{
$cancelled = false;
$fallback = $this->fallback;
$promise = $this->executor->query($query);
return new Promise(function ($resolve, $reject) use (&$promise, $fallback, $query, &$cancelled) {
$promise->then($resolve, function (\Exception $e1) use ($fallback, $query, $resolve, $reject, &$cancelled, &$promise) {
// reject if primary resolution rejected due to cancellation
if ($cancelled) {
$reject($e1);
return;
}
// start fallback query if primary query rejected
$promise = $fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) {
$append = $e2->getMessage();
if (($pos = strpos($append, ':')) !== false) {
$append = substr($append, $pos + 2);
}
// reject with combined error message if both queries fail
$reject(new \RuntimeException($e1->getMessage() . '. ' . $append));
});
});
}, function () use (&$promise, &$cancelled) {
// cancel pending query (primary or fallback)
$cancelled = true;
$promise->cancel();
});
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace React\Dns\Query;
use React\Dns\Config\HostsFile;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Promise;
/**
* Resolves hosts from the given HostsFile or falls back to another executor
*
* If the host is found in the hosts file, it will not be passed to the actual
* DNS executor. If the host is not found in the hosts file, it will be passed
* to the DNS executor as a fallback.
*/
final class HostsFileExecutor implements ExecutorInterface
{
private $hosts;
private $fallback;
public function __construct(HostsFile $hosts, ExecutorInterface $fallback)
{
$this->hosts = $hosts;
$this->fallback = $fallback;
}
public function query(Query $query)
{
if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
// forward lookup for type A or AAAA
$records = array();
$expectsColon = $query->type === Message::TYPE_AAAA;
foreach ($this->hosts->getIpsForHost($query->name) as $ip) {
// ensure this is an IPv4/IPV6 address according to query type
if ((strpos($ip, ':') !== false) === $expectsColon) {
$records[] = new Record($query->name, $query->type, $query->class, 0, $ip);
}
}
if ($records) {
return Promise\resolve(
Message::createResponseWithAnswersForQuery($query, $records)
);
}
} elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) {
// reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain
$ip = $this->getIpFromHost($query->name);
if ($ip !== null) {
$records = array();
foreach ($this->hosts->getHostsForIp($ip) as $host) {
$records[] = new Record($query->name, $query->type, $query->class, 0, $host);
}
if ($records) {
return Promise\resolve(
Message::createResponseWithAnswersForQuery($query, $records)
);
}
}
}
return $this->fallback->query($query);
}
private function getIpFromHost($host)
{
if (substr($host, -13) === '.in-addr.arpa') {
// IPv4: read as IP and reverse bytes
$ip = @inet_pton(substr($host, 0, -13));
if ($ip === false || isset($ip[4])) {
return null;
}
return inet_ntop(strrev($ip));
} elseif (substr($host, -9) === '.ip6.arpa') {
// IPv6: replace dots, reverse nibbles and interpret as hexadecimal string
$ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9)))));
if ($ip === false) {
return null;
}
return $ip;
} else {
return null;
}
}
}

69
vendor/react/dns/src/Query/Query.php vendored Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace React\Dns\Query;
use React\Dns\Model\Message;
/**
* This class represents a single question in a query/response message
*
* It uses a structure similar to `\React\Dns\Message\Record`, but does not
* contain fields for resulting TTL and resulting record data (IPs etc.).
*
* @link https://tools.ietf.org/html/rfc1035#section-4.1.2
* @see \React\Dns\Message\Record
*/
final class Query
{
/**
* @var string query name, i.e. hostname to look up
*/
public $name;
/**
* @var int query type (aka QTYPE), see Message::TYPE_* constants
*/
public $type;
/**
* @var int query class (aka QCLASS), see Message::CLASS_IN constant
*/
public $class;
/**
* @param string $name query name, i.e. hostname to look up
* @param int $type query type, see Message::TYPE_* constants
* @param int $class query class, see Message::CLASS_IN constant
*/
public function __construct($name, $type, $class)
{
$this->name = $name;
$this->type = $type;
$this->class = $class;
}
/**
* Describes the hostname and query type/class for this query
*
* The output format is supposed to be human readable and is subject to change.
* The format is inspired by RFC 3597 when handling unkown types/classes.
*
* @return string "example.com (A)" or "example.com (CLASS0 TYPE1234)"
* @link https://tools.ietf.org/html/rfc3597
*/
public function describe()
{
$class = $this->class !== Message::CLASS_IN ? 'CLASS' . $this->class . ' ' : '';
$type = 'TYPE' . $this->type;
$ref = new \ReflectionClass('React\Dns\Model\Message');
foreach ($ref->getConstants() as $name => $value) {
if ($value === $this->type && \strpos($name, 'TYPE_') === 0) {
$type = \substr($name, 5);
break;
}
}
return $this->name . ' (' . $class . $type . ')';
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace React\Dns\Query;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
final class RetryExecutor implements ExecutorInterface
{
private $executor;
private $retries;
public function __construct(ExecutorInterface $executor, $retries = 2)
{
$this->executor = $executor;
$this->retries = $retries;
}
public function query(Query $query)
{
return $this->tryQuery($query, $this->retries);
}
public function tryQuery(Query $query, $retries)
{
$deferred = new Deferred(function () use (&$promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
});
$success = function ($value) use ($deferred, &$errorback) {
$errorback = null;
$deferred->resolve($value);
};
$executor = $this->executor;
$errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries, $executor) {
if (!$e instanceof TimeoutException) {
$errorback = null;
$deferred->reject($e);
} elseif ($retries <= 0) {
$errorback = null;
$deferred->reject($e = new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: too many retries',
0,
$e
));
// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$r->setAccessible(true);
$trace = $r->getValue($e);
// Exception trace arguments are not available on some PHP 7.4 installs
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($e, $trace);
} else {
--$retries;
$promise = $executor->query($query)->then(
$success,
$errorback
);
}
};
$promise = $this->executor->query($query)->then(
$success,
$errorback
);
return $deferred->promise();
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace React\Dns\Query;
use React\Promise\Promise;
/**
* Send DNS queries over a UDP or TCP/IP stream transport.
*
* This class will automatically choose the correct transport protocol to send
* a DNS query to your DNS server. It will always try to send it over the more
* efficient UDP transport first. If this query yields a size related issue
* (truncated messages), it will retry over a streaming TCP/IP transport.
*
* For more advanced usages one can utilize this class directly.
* The following example looks up the `IPv6` address for `reactphp.org`.
*
* ```php
* $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
*
* $executor->query(
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
* )->then(function (Message $message) {
* foreach ($message->answers as $answer) {
* echo 'IPv6: ' . $answer->data . PHP_EOL;
* }
* }, 'printf');
* ```
*
* Note that this executor only implements the logic to select the correct
* transport for the given DNS query. Implementing the correct transport logic,
* implementing timeouts and any retry logic is left up to the given executors,
* see also [`UdpTransportExecutor`](#udptransportexecutor) and
* [`TcpTransportExecutor`](#tcptransportexecutor) for more details.
*
* Note that this executor is entirely async and as such allows you to execute
* any number of queries concurrently. You should probably limit the number of
* concurrent queries in your application or you're very likely going to face
* rate limitations and bans on the resolver end. For many common applications,
* you may want to avoid sending the same query multiple times when the first
* one is still pending, so you will likely want to use this in combination with
* a `CoopExecutor` like this:
*
* ```php
* $executor = new CoopExecutor(
* new SelectiveTransportExecutor(
* $datagramExecutor,
* $streamExecutor
* )
* );
* ```
*/
class SelectiveTransportExecutor implements ExecutorInterface
{
private $datagramExecutor;
private $streamExecutor;
public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor)
{
$this->datagramExecutor = $datagramExecutor;
$this->streamExecutor = $streamExecutor;
}
public function query(Query $query)
{
$stream = $this->streamExecutor;
$pending = $this->datagramExecutor->query($query);
return new Promise(function ($resolve, $reject) use (&$pending, $stream, $query) {
$pending->then(
$resolve,
function ($e) use (&$pending, $stream, $query, $resolve, $reject) {
if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) {
$pending = $stream->query($query)->then($resolve, $reject);
} else {
$reject($e);
}
}
);
}, function () use (&$pending) {
$pending->cancel();
$pending = null;
});
}
}

View File

@@ -0,0 +1,382 @@
<?php
namespace React\Dns\Query;
use React\Dns\Model\Message;
use React\Dns\Protocol\BinaryDumper;
use React\Dns\Protocol\Parser;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
/**
* Send DNS queries over a TCP/IP stream transport.
*
* This is one of the main classes that send a DNS query to your DNS server.
*
* For more advanced usages one can utilize this class directly.
* The following example looks up the `IPv6` address for `reactphp.org`.
*
* ```php
* $executor = new TcpTransportExecutor('8.8.8.8:53');
*
* $executor->query(
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
* )->then(function (Message $message) {
* foreach ($message->answers as $answer) {
* echo 'IPv6: ' . $answer->data . PHP_EOL;
* }
* }, 'printf');
* ```
*
* See also [example #92](examples).
*
* Note that this executor does not implement a timeout, so you will very likely
* want to use this in combination with a `TimeoutExecutor` like this:
*
* ```php
* $executor = new TimeoutExecutor(
* new TcpTransportExecutor($nameserver),
* 3.0
* );
* ```
*
* Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
* transport, so you do not necessarily have to implement any retry logic.
*
* Note that this executor is entirely async and as such allows you to execute
* queries concurrently. The first query will establish a TCP/IP socket
* connection to the DNS server which will be kept open for a short period.
* Additional queries will automatically reuse this existing socket connection
* to the DNS server, will pipeline multiple requests over this single
* connection and will keep an idle connection open for a short period. The
* initial TCP/IP connection overhead may incur a slight delay if you only send
* occasional queries when sending a larger number of concurrent queries over
* an existing connection, it becomes increasingly more efficient and avoids
* creating many concurrent sockets like the UDP-based executor. You may still
* want to limit the number of (concurrent) queries in your application or you
* may be facing rate limitations and bans on the resolver end. For many common
* applications, you may want to avoid sending the same query multiple times
* when the first one is still pending, so you will likely want to use this in
* combination with a `CoopExecutor` like this:
*
* ```php
* $executor = new CoopExecutor(
* new TimeoutExecutor(
* new TcpTransportExecutor($nameserver),
* 3.0
* )
* );
* ```
*
* > Internally, this class uses PHP's TCP/IP sockets and does not take advantage
* of [react/socket](https://github.com/reactphp/socket) purely for
* organizational reasons to avoid a cyclic dependency between the two
* packages. Higher-level components should take advantage of the Socket
* component instead of reimplementing this socket logic from scratch.
*/
class TcpTransportExecutor implements ExecutorInterface
{
private $nameserver;
private $loop;
private $parser;
private $dumper;
/**
* @var ?resource
*/
private $socket;
/**
* @var Deferred[]
*/
private $pending = array();
/**
* @var string[]
*/
private $names = array();
/**
* Maximum idle time when socket is current unused (i.e. no pending queries outstanding)
*
* If a new query is to be sent during the idle period, we can reuse the
* existing socket without having to wait for a new socket connection.
* This uses a rather small, hard-coded value to not keep any unneeded
* sockets open and to not keep the loop busy longer than needed.
*
* A future implementation may take advantage of `edns-tcp-keepalive` to keep
* the socket open for longer periods. This will likely require explicit
* configuration because this may consume additional resources and also keep
* the loop busy for longer than expected in some applications.
*
* @var float
* @link https://tools.ietf.org/html/rfc7766#section-6.2.1
* @link https://tools.ietf.org/html/rfc7828
*/
private $idlePeriod = 0.001;
/**
* @var ?\React\EventLoop\TimerInterface
*/
private $idleTimer;
private $writeBuffer = '';
private $writePending = false;
private $readBuffer = '';
private $readPending = false;
/** @var string */
private $readChunk = 0xffff;
/**
* @param string $nameserver
* @param ?LoopInterface $loop
*/
public function __construct($nameserver, $loop = null)
{
if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
// several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
$nameserver = '[' . $nameserver . ']';
}
$parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver);
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
throw new \InvalidArgumentException('Invalid nameserver address given');
}
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
$this->loop = $loop ?: Loop::get();
$this->parser = new Parser();
$this->dumper = new BinaryDumper();
}
public function query(Query $query)
{
$request = Message::createRequestForQuery($query);
// keep shuffing message ID to avoid using the same message ID for two pending queries at the same time
while (isset($this->pending[$request->id])) {
$request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore
}
$queryData = $this->dumper->toBinary($request);
$length = \strlen($queryData);
if ($length > 0xffff) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport'
));
}
$queryData = \pack('n', $length) . $queryData;
if ($this->socket === null) {
// create async TCP/IP connection (may take a while)
$socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT);
if ($socket === false) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
));
}
// set socket to non-blocking and wait for it to become writable (connection success/rejected)
\stream_set_blocking($socket, false);
if (\function_exists('stream_set_chunk_size')) {
\stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore
}
$this->socket = $socket;
}
if ($this->idleTimer !== null) {
$this->loop->cancelTimer($this->idleTimer);
$this->idleTimer = null;
}
// wait for socket to become writable to actually write out data
$this->writeBuffer .= $queryData;
if (!$this->writePending) {
$this->writePending = true;
$this->loop->addWriteStream($this->socket, array($this, 'handleWritable'));
}
$names =& $this->names;
$that = $this;
$deferred = new Deferred(function () use ($that, &$names, $request) {
// remove from list of pending names, but remember pending query
$name = $names[$request->id];
unset($names[$request->id]);
$that->checkIdle();
throw new CancellationException('DNS query for ' . $name . ' has been cancelled');
});
$this->pending[$request->id] = $deferred;
$this->names[$request->id] = $query->describe();
return $deferred->promise();
}
/**
* @internal
*/
public function handleWritable()
{
if ($this->readPending === false) {
$name = @\stream_socket_get_name($this->socket, true);
if ($name === false) {
// Connection failed? Check socket error if available for underlying errno/errstr.
// @codeCoverageIgnoreStart
if (\function_exists('socket_import_stream')) {
$socket = \socket_import_stream($this->socket);
$errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
$errstr = \socket_strerror($errno);
} else {
$errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
$errstr = 'Connection refused';
}
// @codeCoverageIgnoreEnd
$this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno);
return;
}
$this->readPending = true;
$this->loop->addReadStream($this->socket, array($this, 'handleRead'));
}
$errno = 0;
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
// Match errstr from PHP's warning message.
// fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe
\preg_match('/errno=(\d+) (.+)/', $error, $m);
$errno = isset($m[1]) ? (int) $m[1] : 0;
$errstr = isset($m[2]) ? $m[2] : $error;
});
$written = \fwrite($this->socket, $this->writeBuffer);
\restore_error_handler();
if ($written === false || $written === 0) {
$this->closeError(
'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
);
return;
}
if (isset($this->writeBuffer[$written])) {
$this->writeBuffer = \substr($this->writeBuffer, $written);
} else {
$this->loop->removeWriteStream($this->socket);
$this->writePending = false;
$this->writeBuffer = '';
}
}
/**
* @internal
*/
public function handleRead()
{
// read one chunk of data from the DNS server
// any error is fatal, this is a stream of TCP/IP data
$chunk = @\fread($this->socket, $this->readChunk);
if ($chunk === false || $chunk === '') {
$this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
return;
}
// reassemble complete message by concatenating all chunks.
$this->readBuffer .= $chunk;
// response message header contains at least 12 bytes
while (isset($this->readBuffer[11])) {
// read response message length from first 2 bytes and ensure we have length + data in buffer
list(, $length) = \unpack('n', $this->readBuffer);
if (!isset($this->readBuffer[$length + 1])) {
return;
}
$data = \substr($this->readBuffer, 2, $length);
$this->readBuffer = (string)substr($this->readBuffer, $length + 2);
try {
$response = $this->parser->parseMessage($data);
} catch (\Exception $e) {
// reject all pending queries if we received an invalid message from remote server
$this->closeError('Invalid message received from DNS server ' . $this->nameserver);
return;
}
// reject all pending queries if we received an unexpected response ID or truncated response
if (!isset($this->pending[$response->id]) || $response->tc) {
$this->closeError('Invalid response message received from DNS server ' . $this->nameserver);
return;
}
$deferred = $this->pending[$response->id];
unset($this->pending[$response->id], $this->names[$response->id]);
$deferred->resolve($response);
$this->checkIdle();
}
}
/**
* @internal
* @param string $reason
* @param int $code
*/
public function closeError($reason, $code = 0)
{
$this->readBuffer = '';
if ($this->readPending) {
$this->loop->removeReadStream($this->socket);
$this->readPending = false;
}
$this->writeBuffer = '';
if ($this->writePending) {
$this->loop->removeWriteStream($this->socket);
$this->writePending = false;
}
if ($this->idleTimer !== null) {
$this->loop->cancelTimer($this->idleTimer);
$this->idleTimer = null;
}
@\fclose($this->socket);
$this->socket = null;
foreach ($this->names as $id => $name) {
$this->pending[$id]->reject(new \RuntimeException(
'DNS query for ' . $name . ' failed: ' . $reason,
$code
));
}
$this->pending = $this->names = array();
}
/**
* @internal
*/
public function checkIdle()
{
if ($this->idleTimer === null && !$this->names) {
$that = $this;
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) {
$that->closeError('Idle timeout');
});
}
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns\Query;
final class TimeoutException extends \Exception
{
}

View File

@@ -0,0 +1,78 @@
<?php
namespace React\Dns\Query;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Promise;
final class TimeoutExecutor implements ExecutorInterface
{
private $executor;
private $loop;
private $timeout;
/**
* @param ExecutorInterface $executor
* @param float $timeout
* @param ?LoopInterface $loop
*/
public function __construct(ExecutorInterface $executor, $timeout, $loop = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->executor = $executor;
$this->loop = $loop ?: Loop::get();
$this->timeout = $timeout;
}
public function query(Query $query)
{
$promise = $this->executor->query($query);
$loop = $this->loop;
$time = $this->timeout;
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $query) {
$timer = null;
$promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
if ($timer) {
$loop->cancelTimer($timer);
}
$timer = false;
$resolve($v);
}, function ($v) use (&$timer, $loop, $reject) {
if ($timer) {
$loop->cancelTimer($timer);
}
$timer = false;
$reject($v);
});
// promise already resolved => no need to start timer
if ($timer === false) {
return;
}
// start timeout timer which will cancel the pending promise
$timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $query) {
$reject(new TimeoutException(
'DNS query for ' . $query->describe() . ' timed out'
));
// Cancel pending query to clean up any underlying resources and references.
// Avoid garbage references in call stack by passing pending promise by reference.
assert(\method_exists($promise, 'cancel'));
$promise->cancel();
$promise = null;
});
}, function () use (&$promise) {
// Cancelling this promise will cancel the pending query, thus triggering the rejection logic above.
// Avoid garbage references in call stack by passing pending promise by reference.
assert(\method_exists($promise, 'cancel'));
$promise->cancel();
$promise = null;
});
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace React\Dns\Query;
use React\Dns\Model\Message;
use React\Dns\Protocol\BinaryDumper;
use React\Dns\Protocol\Parser;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
/**
* Send DNS queries over a UDP transport.
*
* This is the main class that sends a DNS query to your DNS server and is used
* internally by the `Resolver` for the actual message transport.
*
* For more advanced usages one can utilize this class directly.
* The following example looks up the `IPv6` address for `igor.io`.
*
* ```php
* $executor = new UdpTransportExecutor('8.8.8.8:53');
*
* $executor->query(
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
* )->then(function (Message $message) {
* foreach ($message->answers as $answer) {
* echo 'IPv6: ' . $answer->data . PHP_EOL;
* }
* }, 'printf');
* ```
*
* See also the [fourth example](examples).
*
* Note that this executor does not implement a timeout, so you will very likely
* want to use this in combination with a `TimeoutExecutor` like this:
*
* ```php
* $executor = new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* );
* ```
*
* Also note that this executor uses an unreliable UDP transport and that it
* does not implement any retry logic, so you will likely want to use this in
* combination with a `RetryExecutor` like this:
*
* ```php
* $executor = new RetryExecutor(
* new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* )
* );
* ```
*
* Note that this executor is entirely async and as such allows you to execute
* any number of queries concurrently. You should probably limit the number of
* concurrent queries in your application or you're very likely going to face
* rate limitations and bans on the resolver end. For many common applications,
* you may want to avoid sending the same query multiple times when the first
* one is still pending, so you will likely want to use this in combination with
* a `CoopExecutor` like this:
*
* ```php
* $executor = new CoopExecutor(
* new RetryExecutor(
* new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* )
* )
* );
* ```
*
* > Internally, this class uses PHP's UDP sockets and does not take advantage
* of [react/datagram](https://github.com/reactphp/datagram) purely for
* organizational reasons to avoid a cyclic dependency between the two
* packages. Higher-level components should take advantage of the Datagram
* component instead of reimplementing this socket logic from scratch.
*/
final class UdpTransportExecutor implements ExecutorInterface
{
private $nameserver;
private $loop;
private $parser;
private $dumper;
/**
* maximum UDP packet size to send and receive
*
* @var int
*/
private $maxPacketSize = 512;
/**
* @param string $nameserver
* @param ?LoopInterface $loop
*/
public function __construct($nameserver, $loop = null)
{
if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
// several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
$nameserver = '[' . $nameserver . ']';
}
$parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver);
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
throw new \InvalidArgumentException('Invalid nameserver address given');
}
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
$this->loop = $loop ?: Loop::get();
$this->parser = new Parser();
$this->dumper = new BinaryDumper();
}
public function query(Query $query)
{
$request = Message::createRequestForQuery($query);
$queryData = $this->dumper->toBinary($request);
if (isset($queryData[$this->maxPacketSize])) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Query too large for UDP transport',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
// UDP connections are instant, so try connection without a loop or timeout
$errno = 0;
$errstr = '';
$socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0);
if ($socket === false) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
));
}
// set socket to non-blocking and immediately try to send (fill write buffer)
\stream_set_blocking($socket, false);
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
// Write may potentially fail, but most common errors are already caught by connection check above.
// Among others, macOS is known to report here when trying to send to broadcast address.
// This can also be reproduced by writing data exceeding `stream_set_chunk_size()` to a server refusing UDP data.
// fwrite(): send of 8192 bytes failed with errno=111 Connection refused
\preg_match('/errno=(\d+) (.+)/', $error, $m);
$errno = isset($m[1]) ? (int) $m[1] : 0;
$errstr = isset($m[2]) ? $m[2] : $error;
});
$written = \fwrite($socket, $queryData);
\restore_error_handler();
if ($written !== \strlen($queryData)) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
));
}
$loop = $this->loop;
$deferred = new Deferred(function () use ($loop, $socket, $query) {
// cancellation should remove socket from loop and close socket
$loop->removeReadStream($socket);
\fclose($socket);
throw new CancellationException('DNS query for ' . $query->describe() . ' has been cancelled');
});
$max = $this->maxPacketSize;
$parser = $this->parser;
$nameserver = $this->nameserver;
$loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max, $nameserver) {
// try to read a single data packet from the DNS server
// ignoring any errors, this is uses UDP packets and not a stream of data
$data = @\fread($socket, $max);
if ($data === false) {
return;
}
try {
$response = $parser->parseMessage($data);
} catch (\Exception $e) {
// ignore and await next if we received an invalid message from remote server
// this may as well be a fake response from an attacker (possible DOS)
return;
}
// ignore and await next if we received an unexpected response ID
// this may as well be a fake response from an attacker (possible cache poisoning)
if ($response->id !== $request->id) {
return;
}
// we only react to the first valid message, so remove socket from loop and close
$loop->removeReadStream($socket);
\fclose($socket);
if ($response->tc) {
$deferred->reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: The DNS server ' . $nameserver . ' returned a truncated result for a UDP query',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
return;
}
$deferred->resolve($response);
});
return $deferred->promise();
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns;
final class RecordNotFoundException extends \Exception
{
}

View File

@@ -0,0 +1,226 @@
<?php
namespace React\Dns\Resolver;
use React\Cache\ArrayCache;
use React\Cache\CacheInterface;
use React\Dns\Config\Config;
use React\Dns\Config\HostsFile;
use React\Dns\Query\CachingExecutor;
use React\Dns\Query\CoopExecutor;
use React\Dns\Query\ExecutorInterface;
use React\Dns\Query\FallbackExecutor;
use React\Dns\Query\HostsFileExecutor;
use React\Dns\Query\RetryExecutor;
use React\Dns\Query\SelectiveTransportExecutor;
use React\Dns\Query\TcpTransportExecutor;
use React\Dns\Query\TimeoutExecutor;
use React\Dns\Query\UdpTransportExecutor;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
final class Factory
{
/**
* Creates a DNS resolver instance for the given DNS config
*
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
* single nameserver address. If the given config contains more than one DNS
* nameserver, all DNS nameservers will be used in order. The primary DNS
* server will always be used first before falling back to the secondary or
* tertiary DNS server.
*
* @param Config|string $config DNS Config object (recommended) or single nameserver address
* @param ?LoopInterface $loop
* @return \React\Dns\Resolver\ResolverInterface
* @throws \InvalidArgumentException for invalid DNS server address
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
*/
public function create($config, $loop = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$executor = $this->decorateHostsFileExecutor($this->createExecutor($config, $loop ?: Loop::get()));
return new Resolver($executor);
}
/**
* Creates a cached DNS resolver instance for the given DNS config and cache
*
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
* single nameserver address. If the given config contains more than one DNS
* nameserver, all DNS nameservers will be used in order. The primary DNS
* server will always be used first before falling back to the secondary or
* tertiary DNS server.
*
* @param Config|string $config DNS Config object (recommended) or single nameserver address
* @param ?LoopInterface $loop
* @param ?CacheInterface $cache
* @return \React\Dns\Resolver\ResolverInterface
* @throws \InvalidArgumentException for invalid DNS server address
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
*/
public function createCached($config, $loop = null, $cache = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
if ($cache !== null && !$cache instanceof CacheInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #3 ($cache) expected null|React\Cache\CacheInterface');
}
// default to keeping maximum of 256 responses in cache unless explicitly given
if (!($cache instanceof CacheInterface)) {
$cache = new ArrayCache(256);
}
$executor = $this->createExecutor($config, $loop ?: Loop::get());
$executor = new CachingExecutor($executor, $cache);
$executor = $this->decorateHostsFileExecutor($executor);
return new Resolver($executor);
}
/**
* Tries to load the hosts file and decorates the given executor on success
*
* @param ExecutorInterface $executor
* @return ExecutorInterface
* @codeCoverageIgnore
*/
private function decorateHostsFileExecutor(ExecutorInterface $executor)
{
try {
$executor = new HostsFileExecutor(
HostsFile::loadFromPathBlocking(),
$executor
);
} catch (\RuntimeException $e) {
// ignore this file if it can not be loaded
}
// Windows does not store localhost in hosts file by default but handles this internally
// To compensate for this, we explicitly use hard-coded defaults for localhost
if (DIRECTORY_SEPARATOR === '\\') {
$executor = new HostsFileExecutor(
new HostsFile("127.0.0.1 localhost\n::1 localhost"),
$executor
);
}
return $executor;
}
/**
* @param Config|string $nameserver
* @param LoopInterface $loop
* @return CoopExecutor
* @throws \InvalidArgumentException for invalid DNS server address
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
*/
private function createExecutor($nameserver, LoopInterface $loop)
{
if ($nameserver instanceof Config) {
if (!$nameserver->nameservers) {
throw new \UnderflowException('Empty config with no DNS servers');
}
// Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config).
// Note to future self: Recursion isn't too hard, but how deep do we really want to go?
$primary = reset($nameserver->nameservers);
$secondary = next($nameserver->nameservers);
$tertiary = next($nameserver->nameservers);
if ($tertiary !== false) {
// 3 DNS servers given => nest first with fallback for second and third
return new CoopExecutor(
new RetryExecutor(
new FallbackExecutor(
$this->createSingleExecutor($primary, $loop),
new FallbackExecutor(
$this->createSingleExecutor($secondary, $loop),
$this->createSingleExecutor($tertiary, $loop)
)
)
)
);
} elseif ($secondary !== false) {
// 2 DNS servers given => fallback from first to second
return new CoopExecutor(
new RetryExecutor(
new FallbackExecutor(
$this->createSingleExecutor($primary, $loop),
$this->createSingleExecutor($secondary, $loop)
)
)
);
} else {
// 1 DNS server given => use single executor
$nameserver = $primary;
}
}
return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop)));
}
/**
* @param string $nameserver
* @param LoopInterface $loop
* @return ExecutorInterface
* @throws \InvalidArgumentException for invalid DNS server address
*/
private function createSingleExecutor($nameserver, LoopInterface $loop)
{
$parts = \parse_url($nameserver);
if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') {
$executor = $this->createTcpExecutor($nameserver, $loop);
} elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') {
$executor = $this->createUdpExecutor($nameserver, $loop);
} else {
$executor = new SelectiveTransportExecutor(
$this->createUdpExecutor($nameserver, $loop),
$this->createTcpExecutor($nameserver, $loop)
);
}
return $executor;
}
/**
* @param string $nameserver
* @param LoopInterface $loop
* @return TimeoutExecutor
* @throws \InvalidArgumentException for invalid DNS server address
*/
private function createTcpExecutor($nameserver, LoopInterface $loop)
{
return new TimeoutExecutor(
new TcpTransportExecutor($nameserver, $loop),
5.0,
$loop
);
}
/**
* @param string $nameserver
* @param LoopInterface $loop
* @return TimeoutExecutor
* @throws \InvalidArgumentException for invalid DNS server address
*/
private function createUdpExecutor($nameserver, LoopInterface $loop)
{
return new TimeoutExecutor(
new UdpTransportExecutor(
$nameserver,
$loop
),
5.0,
$loop
);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace React\Dns\Resolver;
use React\Dns\Model\Message;
use React\Dns\Query\ExecutorInterface;
use React\Dns\Query\Query;
use React\Dns\RecordNotFoundException;
/**
* @see ResolverInterface for the base interface
*/
final class Resolver implements ResolverInterface
{
private $executor;
public function __construct(ExecutorInterface $executor)
{
$this->executor = $executor;
}
public function resolve($domain)
{
return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
return $ips[array_rand($ips)];
});
}
public function resolveAll($domain, $type)
{
$query = new Query($domain, $type, Message::CLASS_IN);
$that = $this;
return $this->executor->query(
$query
)->then(function (Message $response) use ($query, $that) {
return $that->extractValues($query, $response);
});
}
/**
* [Internal] extract all resource record values from response for this query
*
* @param Query $query
* @param Message $response
* @return array
* @throws RecordNotFoundException when response indicates an error or contains no data
* @internal
*/
public function extractValues(Query $query, Message $response)
{
// reject if response code indicates this is an error response message
$code = $response->rcode;
if ($code !== Message::RCODE_OK) {
switch ($code) {
case Message::RCODE_FORMAT_ERROR:
$message = 'Format Error';
break;
case Message::RCODE_SERVER_FAILURE:
$message = 'Server Failure';
break;
case Message::RCODE_NAME_ERROR:
$message = 'Non-Existent Domain / NXDOMAIN';
break;
case Message::RCODE_NOT_IMPLEMENTED:
$message = 'Not Implemented';
break;
case Message::RCODE_REFUSED:
$message = 'Refused';
break;
default:
$message = 'Unknown error response code ' . $code;
}
throw new RecordNotFoundException(
'DNS query for ' . $query->describe() . ' returned an error response (' . $message . ')',
$code
);
}
$answers = $response->answers;
$addresses = $this->valuesByNameAndType($answers, $query->name, $query->type);
// reject if we did not receive a valid answer (domain is valid, but no record for this type could be found)
if (0 === count($addresses)) {
throw new RecordNotFoundException(
'DNS query for ' . $query->describe() . ' did not return a valid answer (NOERROR / NODATA)'
);
}
return array_values($addresses);
}
/**
* @param \React\Dns\Model\Record[] $answers
* @param string $name
* @param int $type
* @return array
*/
private function valuesByNameAndType(array $answers, $name, $type)
{
// return all record values for this name and type (if any)
$named = $this->filterByName($answers, $name);
$records = $this->filterByType($named, $type);
if ($records) {
return $this->mapRecordData($records);
}
// no matching records found? check if there are any matching CNAMEs instead
$cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
if ($cnameRecords) {
$cnames = $this->mapRecordData($cnameRecords);
foreach ($cnames as $cname) {
$records = array_merge(
$records,
$this->valuesByNameAndType($answers, $cname, $type)
);
}
}
return $records;
}
private function filterByName(array $answers, $name)
{
return $this->filterByField($answers, 'name', $name);
}
private function filterByType(array $answers, $type)
{
return $this->filterByField($answers, 'type', $type);
}
private function filterByField(array $answers, $field, $value)
{
$value = strtolower($value);
return array_filter($answers, function ($answer) use ($field, $value) {
return $value === strtolower($answer->$field);
});
}
private function mapRecordData(array $records)
{
return array_map(function ($record) {
return $record->data;
}, $records);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace React\Dns\Resolver;
interface ResolverInterface
{
/**
* Resolves the given $domain name to a single IPv4 address (type `A` query).
*
* ```php
* $resolver->resolve('reactphp.org')->then(function ($ip) {
* echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
* });
* ```
*
* This is one of the main methods in this package. It sends a DNS query
* for the given $domain name to your DNS server and returns a single IP
* address on success.
*
* If the DNS server sends a DNS response message that contains more than
* one IP address for this query, it will randomly pick one of the IP
* addresses from the response. If you want the full list of IP addresses
* or want to send a different type of query, you should use the
* [`resolveAll()`](#resolveall) method instead.
*
* If the DNS server sends a DNS response message that indicates an error
* code, this method will reject with a `RecordNotFoundException`. Its
* message and code can be used to check for the response code.
*
* If the DNS communication fails and the server does not respond with a
* valid response message, this message will reject with an `Exception`.
*
* Pending DNS queries can be cancelled by cancelling its pending promise like so:
*
* ```php
* $promise = $resolver->resolve('reactphp.org');
*
* $promise->cancel();
* ```
*
* @param string $domain
* @return \React\Promise\PromiseInterface<string>
* resolves with a single IP address on success or rejects with an Exception on error.
*/
public function resolve($domain);
/**
* Resolves all record values for the given $domain name and query $type.
*
* ```php
* $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
* echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
* });
*
* $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
* echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
* });
* ```
*
* This is one of the main methods in this package. It sends a DNS query
* for the given $domain name to your DNS server and returns a list with all
* record values on success.
*
* If the DNS server sends a DNS response message that contains one or more
* records for this query, it will return a list with all record values
* from the response. You can use the `Message::TYPE_*` constants to control
* which type of query will be sent. Note that this method always returns a
* list of record values, but each record value type depends on the query
* type. For example, it returns the IPv4 addresses for type `A` queries,
* the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
* `CNAME` and `PTR` queries and structured data for other queries. See also
* the `Record` documentation for more details.
*
* If the DNS server sends a DNS response message that indicates an error
* code, this method will reject with a `RecordNotFoundException`. Its
* message and code can be used to check for the response code.
*
* If the DNS communication fails and the server does not respond with a
* valid response message, this message will reject with an `Exception`.
*
* Pending DNS queries can be cancelled by cancelling its pending promise like so:
*
* ```php
* $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
*
* $promise->cancel();
* ```
*
* @param string $domain
* @return \React\Promise\PromiseInterface<array>
* Resolves with all record values on success or rejects with an Exception on error.
*/
public function resolveAll($domain, $type);
}

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

785
vendor/react/socket/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,785 @@
# Changelog
## 1.16.0 (2024-07-26)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
(#318 by @clue)
## 1.15.0 (2023-12-15)
* Feature: Full PHP 8.3 compatibility.
(#310 by @clue)
* Fix: Fix cancelling during the 50ms resolution delay when DNS is still pending.
(#311 by @clue)
## 1.14.0 (2023-08-25)
* Feature: Improve Promise v3 support and use template types.
(#307 and #309 by @clue)
* Improve test suite and update to collect all garbage cycles.
(#308 by @clue)
## 1.13.0 (2023-06-07)
* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
(#305 by @clue)
* Feature: Improve errno detection for failed connections without `ext-sockets`.
(#304 by @clue)
* Improve test suite, clean up leftover `.sock` files and report failed assertions.
(#299, #300, #301 and #306 by @clue)
## 1.12.0 (2022-08-25)
* Feature: Forward compatibility with react/promise 3.
(#214 by @WyriHaximus and @clue)
* Feature: Full support for PHP 8.2 release.
(#298 by @WyriHaximus)
* Feature: Avoid unneeded syscall on socket close.
(#292 by @clue)
* Feature / Fix: Improve error reporting when custom error handler is used.
(#290 by @clue)
* Fix: Fix invalid references in exception stack trace.
(#284 by @clue)
* Minor documentation improvements, update to use new reactphp/async package instead of clue/reactphp-block.
(#296 by @clue, #285 by @SimonFrings and #295 by @nhedger)
* Improve test suite, update macOS and HHVM environment, fix optional tests for `ENETUNREACH`.
(#288, #289 and #297 by @clue)
## 1.11.0 (2022-01-14)
* Feature: Full support for PHP 8.1 release.
(#277 by @clue)
* Feature: Avoid dependency on `ext-filter`.
(#279 by @clue)
* Improve test suite to skip FD test when hitting memory limit
and skip legacy TLS 1.0 tests if disabled by system.
(#278 and #281 by @clue and #283 by @SimonFrings)
## 1.10.0 (2021-11-29)
* Feature: Support listening on existing file descriptors (FDs) with `SocketServer`.
(#269 by @clue)
```php
$socket = new React\Socket\SocketSever('php://fd/3');
```
This is particularly useful when using [systemd socket activation](https://www.freedesktop.org/software/systemd/man/systemd.socket.html) like this:
```bash
$ systemd-socket-activate -l 8000 php examples/03-http-server.php php://fd/3
```
* Feature: Improve error messages for failed connection attempts with `errno` and `errstr`.
(#265, #266, #267, #270 and #271 by @clue and #268 by @SimonFrings)
All error messages now always include the appropriate `errno` and `errstr` to
give more details about the error reason when available. Along with these
error details exposed by the underlying system functions, it will also
include the appropriate error constant name (such as `ECONNREFUSED`) when
available. Accordingly, failed TCP/IP connections will now report the actual
underlying error condition instead of a generic "Connection refused" error.
Higher-level error messages will now consistently report the connection URI
scheme and hostname used in all error messages.
For most common use cases this means that simply reporting the `Exception`
message should give the most relevant details for any connection issues:
```php
$connector = new React\Socket\Connector();
$connector->connect($uri)->then(function (React\Socket\ConnectionInterface $conn) {
// …
}, function (Exception $e) {
echo 'Error:' . $e->getMessage() . PHP_EOL;
});
```
* Improve test suite, test against PHP 8.1 release.
(#274 by @SimonFrings)
## 1.9.0 (2021-08-03)
* Feature: Add new `SocketServer` and deprecate `Server` to avoid class name collisions.
(#263 by @clue)
The new `SocketServer` class has been added with an improved constructor signature
as a replacement for the previous `Server` class in order to avoid any ambiguities.
The previous name has been deprecated and should not be used anymore.
In its most basic form, the deprecated `Server` can now be considered an alias for new `SocketServer`.
```php
// deprecated
$socket = new React\Socket\Server(0);
$socket = new React\Socket\Server('127.0.0.1:8000');
$socket = new React\Socket\Server('127.0.0.1:8000', null, $context);
$socket = new React\Socket\Server('127.0.0.1:8000', $loop, $context);
// new
$socket = new React\Socket\SocketServer('127.0.0.1:0');
$socket = new React\Socket\SocketServer('127.0.0.1:8000');
$socket = new React\Socket\SocketServer('127.0.0.1:8000', $context);
$socket = new React\Socket\SocketServer('127.0.0.1:8000', $context, $loop);
```
* Feature: Update `Connector` signature to take optional `$context` as first argument.
(#264 by @clue)
The new signature has been added to match the new `SocketServer` and
consistently move the now commonly unneeded loop argument to the last argument.
The previous signature has been deprecated and should not be used anymore.
In its most basic form, both signatures are compatible.
```php
// deprecated
$connector = new React\Socket\Connector(null, $context);
$connector = new React\Socket\Connector($loop, $context);
// new
$connector = new React\Socket\Connector($context);
$connector = new React\Socket\Connector($context, $loop);
```
## 1.8.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).
(#260 by @clue)
```php
// old (still supported)
$socket = new React\Socket\Server('127.0.0.1:8080', $loop);
$connector = new React\Socket\Connector($loop);
// new (using default loop)
$socket = new React\Socket\Server('127.0.0.1:8080');
$connector = new React\Socket\Connector();
```
## 1.7.0 (2021-06-25)
* Feature: Support falling back to multiple DNS servers from DNS config.
(#257 by @clue)
If you're using the default `Connector`, it will now use all DNS servers
configured on your system. If you have multiple DNS servers configured and
connectivity to the primary DNS server is broken, it will now fall back to
your other DNS servers, thus providing improved connectivity and redundancy
for broken DNS configurations.
* Feature: Use round robin for happy eyeballs DNS responses (load balancing).
(#247 by @clue)
If you're using the default `Connector`, it will now randomize the order of
the IP addresses resolved via DNS when connecting. This allows the load to
be distributed more evenly across all returned IP addresses. This can be
used as a very basic DNS load balancing mechanism.
* Internal improvement to avoid unhandled rejection for future Promise API.
(#258 by @clue)
* Improve test suite, use GitHub actions for continuous integration (CI).
(#254 by @SimonFrings)
## 1.6.0 (2020-08-28)
* Feature: Support upcoming PHP 8 release.
(#246 by @clue)
* Feature: Change default socket backlog size to 511.
(#242 by @clue)
* Fix: Fix closing connection when cancelling during TLS handshake.
(#241 by @clue)
* Fix: Fix blocking during possible `accept()` race condition
when multiple socket servers listen on same socket address.
(#244 by @clue)
* Improve test suite, update PHPUnit config and add full core team to the license.
(#243 by @SimonFrings and #245 by @WyriHaximus)
## 1.5.0 (2020-07-01)
* Feature / Fix: Improve error handling and reporting for happy eyeballs and
immediately try next connection when one connection attempt fails.
(#230, #231, #232 and #233 by @clue)
Error messages for failed connection attempts now include more details to
ease debugging. Additionally, the happy eyeballs algorithm has been improved
to avoid having to wait for some timers to expire which significantly
improves connection setup times (in particular when IPv6 isn't available).
* Improve test suite, minor code cleanup and improve code coverage to 100%.
Update to PHPUnit 9 and skip legacy TLS 1.0 / TLS 1.1 tests if disabled by
system. Run tests on Windows and simplify Travis CI test matrix for Mac OS X
setup and skip all TLS tests on legacy HHVM.
(#229, #235, #236 and #238 by @clue and #239 by @SimonFrings)
## 1.4.0 (2020-03-12)
A major new feature release, see [**release announcement**](https://clue.engineering/2020/introducing-ipv6-for-reactphp).
* Feature: Add IPv6 support to `Connector` (implement "Happy Eyeballs" algorithm to support IPv6 probing).
IPv6 support is turned on by default, use new `happy_eyeballs` option in `Connector` to toggle behavior.
(#196, #224 and #225 by @WyriHaximus and @clue)
* Feature: Default to using DNS cache (with max 256 entries) for `Connector`.
(#226 by @clue)
* Add `.gitattributes` to exclude dev files from exports and some minor code style fixes.
(#219 by @reedy and #218 by @mmoreram)
* Improve test suite to fix failing test cases when using new DNS component,
significantly improve test performance by awaiting events instead of sleeping,
exclude TLS 1.3 test on PHP 7.3, run tests on PHP 7.4 and simplify test matrix.
(#208, #209, #210, #217 and #223 by @clue)
## 1.3.0 (2019-07-10)
* Feature: Forward compatibility with upcoming stable DNS component.
(#206 by @clue)
## 1.2.1 (2019-06-03)
* Avoid uneeded fragmented TLS work around for PHP 7.3.3+ and
work around failing test case detecting EOF on TLS 1.3 socket streams.
(#201 and #202 by @clue)
* Improve TLS certificate/passphrase example.
(#190 by @jsor)
## 1.2.0 (2019-01-07)
* Feature / Fix: Improve TLS 1.3 support.
(#186 by @clue)
TLS 1.3 is now an official standard as of August 2018! :tada:
The protocol has major improvements in the areas of security, performance, and privacy.
TLS 1.3 is supported by default as of [OpenSSL 1.1.1](https://www.openssl.org/blog/blog/2018/09/11/release111/).
For example, this version ships with Ubuntu 18.10 (and newer) by default, meaning that recent installations support TLS 1.3 out of the box :shipit:
* Fix: Avoid possibility of missing remote address when TLS handshake fails.
(#188 by @clue)
* Improve performance by prefixing all global functions calls with `\` to skip the look up and resolve process and go straight to the global function.
(#183 by @WyriHaximus)
* Update documentation to use full class names with namespaces.
(#187 by @clue)
* Improve test suite to avoid some possible race conditions,
test against PHP 7.3 on Travis and
use dedicated `assertInstanceOf()` assertions.
(#185 by @clue, #178 by @WyriHaximus and #181 by @carusogabriel)
## 1.1.0 (2018-10-01)
* Feature: Improve error reporting for failed connection attempts and improve
cancellation forwarding during DNS lookup, TCP/IP connection or TLS handshake.
(#168, #169, #170, #171, #176 and #177 by @clue)
All error messages now always contain a reference to the remote URI to give
more details which connection actually failed and the reason for this error.
Accordingly, failures during DNS lookup will now mention both the remote URI
as well as the DNS error reason. TCP/IP connection issues and errors during
a secure TLS handshake will both mention the remote URI as well as the
underlying socket error. Similarly, lost/dropped connections during a TLS
handshake will now report a lost connection instead of an empty error reason.
For most common use cases this means that simply reporting the `Exception`
message should give the most relevant details for any connection issues:
```php
$promise = $connector->connect('tls://example.com:443');
$promise->then(function (ConnectionInterface $conn) use ($loop) {
// …
}, function (Exception $e) {
echo $e->getMessage();
});
```
## 1.0.0 (2018-07-11)
* 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.
> Contains no other changes, so it's actually fully compatible with the v0.8.12 release.
## 0.8.12 (2018-06-11)
* Feature: Improve memory consumption for failed and cancelled connection attempts.
(#161 by @clue)
* Improve test suite to fix Travis config to test against legacy PHP 5.3 again.
(#162 by @clue)
## 0.8.11 (2018-04-24)
* Feature: Improve memory consumption for cancelled connection attempts and
simplify skipping DNS lookup when connecting to IP addresses.
(#159 and #160 by @clue)
## 0.8.10 (2018-02-28)
* Feature: Update DNS dependency to support loading system default DNS
nameserver config on all supported platforms
(`/etc/resolv.conf` on Unix/Linux/Mac/Docker/WSL and WMIC on Windows)
(#152 by @clue)
This means that connecting to hosts that are managed by a local DNS server,
such as a corporate DNS server or when using Docker containers, will now
work as expected across all platforms with no changes required:
```php
$connector = new Connector($loop);
$connector->connect('intranet.example:80')->then(function ($connection) {
// …
});
```
## 0.8.9 (2018-01-18)
* Feature: Support explicitly choosing TLS version to negotiate with remote side
by respecting `crypto_method` context parameter for all classes.
(#149 by @clue)
By default, all connector and server classes support TLSv1.0+ and exclude
support for legacy SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly
choose the TLS version you want to negotiate with the remote side:
```php
// new: now supports 'crypto_method` context parameter for all classes
$connector = new Connector($loop, array(
'tls' => array(
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
)
));
```
* Minor internal clean up to unify class imports
(#148 by @clue)
## 0.8.8 (2018-01-06)
* Improve test suite by adding test group to skip integration tests relying on
internet connection and fix minor documentation typo.
(#146 by @clue and #145 by @cn007b)
## 0.8.7 (2017-12-24)
* Fix: Fix closing socket resource before removing from loop
(#141 by @clue)
This fixes the root cause of an uncaught `Exception` that only manifested
itself after the recent Stream v0.7.4 component update and only if you're
using `ext-event` (`ExtEventLoop`).
* Improve test suite by testing against PHP 7.2
(#140 by @carusogabriel)
## 0.8.6 (2017-11-18)
* Feature: Add Unix domain socket (UDS) support to `Server` with `unix://` URI scheme
and add advanced `UnixServer` class.
(#120 by @andig)
```php
// new: Server now supports "unix://" scheme
$server = new Server('unix:///tmp/server.sock', $loop);
// new: advanced usage
$server = new UnixServer('/tmp/server.sock', $loop);
```
* Restructure examples to ease getting started
(#136 by @clue)
* Improve test suite by adding forward compatibility with PHPUnit 6 and
ignore Mac OS X test failures for now until Travis tests work again
(#133 by @gabriel-caruso and #134 by @clue)
## 0.8.5 (2017-10-23)
* Fix: Work around PHP bug with Unix domain socket (UDS) paths for Mac OS X
(#123 by @andig)
* Fix: Fix `SecureServer` to return `null` URI if server socket is already closed
(#129 by @clue)
* Improve test suite by adding forward compatibility with PHPUnit v5 and
forward compatibility with upcoming EventLoop releases in tests and
test Mac OS X on Travis
(#122 by @andig and #125, #127 and #130 by @clue)
* Readme improvements
(#118 by @jsor)
## 0.8.4 (2017-09-16)
* Feature: Add `FixedUriConnector` decorator to use fixed, preconfigured URI instead
(#117 by @clue)
This can be useful for consumers that do not support certain URIs, such as
when you want to explicitly connect to a Unix domain socket (UDS) path
instead of connecting to a default address assumed by an higher-level API:
```php
$connector = new FixedUriConnector(
'unix:///var/run/docker.sock',
new UnixConnector($loop)
);
// destination will be ignored, actually connects to Unix domain socket
$promise = $connector->connect('localhost:80');
```
## 0.8.3 (2017-09-08)
* Feature: Reduce memory consumption for failed connections
(#113 by @valga)
* Fix: Work around write chunk size for TLS streams for PHP < 7.1.14
(#114 by @clue)
## 0.8.2 (2017-08-25)
* Feature: Update DNS dependency to support hosts file on all platforms
(#112 by @clue)
This means that connecting to hosts such as `localhost` will now work as
expected across all platforms with no changes required:
```php
$connector = new Connector($loop);
$connector->connect('localhost:8080')->then(function ($connection) {
// …
});
```
## 0.8.1 (2017-08-15)
* Feature: Forward compatibility with upcoming EventLoop v1.0 and v0.5 and
target evenement 3.0 a long side 2.0 and 1.0
(#104 by @clue and #111 by @WyriHaximus)
* Improve test suite by locking Travis distro so new defaults will not break the build and
fix HHVM build for now again and ignore future HHVM build errors
(#109 and #110 by @clue)
* Minor documentation fixes
(#103 by @christiaan and #108 by @hansott)
## 0.8.0 (2017-05-09)
* Feature: New `Server` class now acts as a facade for existing server classes
and renamed old `Server` to `TcpServer` for advanced usage.
(#96 and #97 by @clue)
The `Server` class is now the main class in this package that implements the
`ServerInterface` and allows you to accept incoming streaming connections,
such as plaintext TCP/IP or secure TLS connection streams.
> This is not a BC break and consumer code does not have to be updated.
* Feature / BC break: All addresses are now URIs that include the URI scheme
(#98 by @clue)
```diff
- $parts = parse_url('tcp://' . $conn->getRemoteAddress());
+ $parts = parse_url($conn->getRemoteAddress());
```
* Fix: Fix `unix://` addresses for Unix domain socket (UDS) paths
(#100 by @clue)
* Feature: Forward compatibility with Stream v1.0 and v0.7
(#99 by @clue)
## 0.7.2 (2017-04-24)
* Fix: Work around latest PHP 7.0.18 and 7.1.4 no longer accepting full URIs
(#94 by @clue)
## 0.7.1 (2017-04-10)
* Fix: Ignore HHVM errors when closing connection that is already closing
(#91 by @clue)
## 0.7.0 (2017-04-10)
* Feature: Merge SocketClient component into this component
(#87 by @clue)
This means that this package now provides async, streaming plaintext TCP/IP
and secure TLS socket server and client connections for ReactPHP.
```
$connector = new React\Socket\Connector($loop);
$connector->connect('google.com:80')->then(function (ConnectionInterface $conn) {
$connection->write('…');
});
```
Accordingly, the `ConnectionInterface` is now used to represent both incoming
server side connections as well as outgoing client side connections.
If you've previously used the SocketClient component to establish outgoing
client connections, upgrading should take no longer than a few minutes.
All classes have been merged as-is from the latest `v0.7.0` release with no
other changes, so you can simply update your code to use the updated namespace
like this:
```php
// old from SocketClient component and namespace
$connector = new React\SocketClient\Connector($loop);
$connector->connect('google.com:80')->then(function (ConnectionInterface $conn) {
$connection->write('…');
});
// new
$connector = new React\Socket\Connector($loop);
$connector->connect('google.com:80')->then(function (ConnectionInterface $conn) {
$connection->write('…');
});
```
## 0.6.0 (2017-04-04)
* Feature: Add `LimitingServer` to limit and keep track of open connections
(#86 by @clue)
```php
$server = new Server(0, $loop);
$server = new LimitingServer($server, 100);
$server->on('connection', function (ConnectionInterface $connection) {
$connection->write('hello there!' . PHP_EOL);
});
```
* Feature / BC break: Add `pause()` and `resume()` methods to limit active
connections
(#84 by @clue)
```php
$server = new Server(0, $loop);
$server->pause();
$loop->addTimer(1.0, function() use ($server) {
$server->resume();
});
```
## 0.5.1 (2017-03-09)
* Feature: Forward compatibility with Stream v0.5 and upcoming v0.6
(#79 by @clue)
## 0.5.0 (2017-02-14)
* Feature / BC break: Replace `listen()` call with URIs passed to constructor
and reject listening on hostnames with `InvalidArgumentException`
and replace `ConnectionException` with `RuntimeException` for consistency
(#61, #66 and #72 by @clue)
```php
// old
$server = new Server($loop);
$server->listen(8080);
// new
$server = new Server(8080, $loop);
```
Similarly, you can now pass a full listening URI to the constructor to change
the listening host:
```php
// old
$server = new Server($loop);
$server->listen(8080, '127.0.0.1');
// new
$server = new Server('127.0.0.1:8080', $loop);
```
Trying to start listening on (DNS) host names will now throw an
`InvalidArgumentException`, use IP addresses instead:
```php
// old
$server = new Server($loop);
$server->listen(8080, 'localhost');
// new
$server = new Server('127.0.0.1:8080', $loop);
```
If trying to listen fails (such as if port is already in use or port below
1024 may require root access etc.), it will now throw a `RuntimeException`,
the `ConnectionException` class has been removed:
```php
// old: throws React\Socket\ConnectionException
$server = new Server($loop);
$server->listen(80);
// new: throws RuntimeException
$server = new Server(80, $loop);
```
* Feature / BC break: Rename `shutdown()` to `close()` for consistency throughout React
(#62 by @clue)
```php
// old
$server->shutdown();
// new
$server->close();
```
* Feature / BC break: Replace `getPort()` with `getAddress()`
(#67 by @clue)
```php
// old
echo $server->getPort(); // 8080
// new
echo $server->getAddress(); // 127.0.0.1:8080
```
* Feature / BC break: `getRemoteAddress()` returns full address instead of only IP
(#65 by @clue)
```php
// old
echo $connection->getRemoteAddress(); // 192.168.0.1
// new
echo $connection->getRemoteAddress(); // 192.168.0.1:51743
```
* Feature / BC break: Add `getLocalAddress()` method
(#68 by @clue)
```php
echo $connection->getLocalAddress(); // 127.0.0.1:8080
```
* BC break: The `Server` and `SecureServer` class are now marked `final`
and you can no longer `extend` them
(which was never documented or recommended anyway).
Public properties and event handlers are now internal only.
Please use composition instead of extension.
(#71, #70 and #69 by @clue)
## 0.4.6 (2017-01-26)
* Feature: Support socket context options passed to `Server`
(#64 by @clue)
* Fix: Properly return `null` for unknown addresses
(#63 by @clue)
* Improve documentation for `ServerInterface` and lock test suite requirements
(#60 by @clue, #57 by @shaunbramley)
## 0.4.5 (2017-01-08)
* Feature: Add `SecureServer` for secure TLS connections
(#55 by @clue)
* Add functional integration tests
(#54 by @clue)
## 0.4.4 (2016-12-19)
* Feature / Fix: `ConnectionInterface` should extend `DuplexStreamInterface` + documentation
(#50 by @clue)
* Feature / Fix: Improve test suite and switch to normal stream handler
(#51 by @clue)
* Feature: Add examples
(#49 by @clue)
## 0.4.3 (2016-03-01)
* Bug fix: Suppress errors on stream_socket_accept to prevent PHP from crashing
* Support for PHP7 and HHVM
* Support PHP 5.3 again
## 0.4.2 (2014-05-25)
* Verify stream is a valid resource in Connection
## 0.4.1 (2014-04-13)
* Bug fix: Check read buffer for data before shutdown signal and end emit (@ArtyDev)
* Bug fix: v0.3.4 changes merged for v0.4.1
## 0.3.4 (2014-03-30)
* Bug fix: Reset socket to non-blocking after shutting down (PHP bug)
## 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.3 (2013-07-08)
* Version bump
## 0.3.2 (2013-05-10)
* Version bump
## 0.3.1 (2013-04-21)
* Feature: Support binding to IPv6 addresses (@clue)
## 0.3.0 (2013-04-14)
* Bump React dependencies to v0.3
## 0.2.6 (2012-12-26)
* Version bump
## 0.2.3 (2012-11-14)
* Version bump
## 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/socket/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.

1564
vendor/react/socket/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

52
vendor/react/socket/composer.json vendored Normal file
View File

@@ -0,0 +1,52 @@
{
"name": "react/socket",
"description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP",
"keywords": ["async", "socket", "stream", "connection", "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",
"react/dns": "^1.13",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.6 || ^1.2.1",
"react/stream": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3.3 || ^2",
"react/promise-stream": "^1.4",
"react/promise-timer": "^1.11"
},
"autoload": {
"psr-4": {
"React\\Socket\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Socket\\": "tests/"
}
}
}

183
vendor/react/socket/src/Connection.php vendored Normal file
View File

@@ -0,0 +1,183 @@
<?php
namespace React\Socket;
use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use React\Stream\DuplexResourceStream;
use React\Stream\Util;
use React\Stream\WritableResourceStream;
use React\Stream\WritableStreamInterface;
/**
* The actual connection implementation for ConnectionInterface
*
* This class should only be used internally, see ConnectionInterface instead.
*
* @see ConnectionInterface
* @internal
*/
class Connection extends EventEmitter implements ConnectionInterface
{
/**
* Internal flag whether this is a Unix domain socket (UDS) connection
*
* @internal
*/
public $unix = false;
/**
* Internal flag whether encryption has been enabled on this connection
*
* Mostly used by internal StreamEncryption so that connection returns
* `tls://` scheme for encrypted connections instead of `tcp://`.
*
* @internal
*/
public $encryptionEnabled = false;
/** @internal */
public $stream;
private $input;
public function __construct($resource, LoopInterface $loop)
{
// PHP < 7.3.3 (and PHP < 7.2.15) suffers from a bug where feof() might
// block with 100% CPU usage on fragmented TLS records.
// We try to work around this by always consuming the complete receive
// buffer at once to avoid stale data in TLS buffers. This is known to
// work around high CPU usage for well-behaving peers, but this may
// cause very large data chunks for high throughput scenarios. The buggy
// behavior can still be triggered due to network I/O buffers or
// malicious peers on affected versions, upgrading is highly recommended.
// @link https://bugs.php.net/bug.php?id=77390
$clearCompleteBuffer = \PHP_VERSION_ID < 70215 || (\PHP_VERSION_ID >= 70300 && \PHP_VERSION_ID < 70303);
// PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big
// chunks of data over TLS streams at once.
// We try to work around this by limiting the write chunk size to 8192
// bytes for older PHP versions only.
// This is only a work-around and has a noticable performance penalty on
// affected versions. Please update your PHP version.
// This applies to all streams because TLS may be enabled later on.
// See https://github.com/reactphp/socket/issues/105
$limitWriteChunks = (\PHP_VERSION_ID < 70018 || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70104));
$this->input = new DuplexResourceStream(
$resource,
$loop,
$clearCompleteBuffer ? -1 : null,
new WritableResourceStream($resource, $loop, null, $limitWriteChunks ? 8192 : null)
);
$this->stream = $resource;
Util::forwardEvents($this->input, $this, array('data', 'end', 'error', 'close', 'pipe', 'drain'));
$this->input->on('close', array($this, 'close'));
}
public function isReadable()
{
return $this->input->isReadable();
}
public function isWritable()
{
return $this->input->isWritable();
}
public function pause()
{
$this->input->pause();
}
public function resume()
{
$this->input->resume();
}
public function pipe(WritableStreamInterface $dest, array $options = array())
{
return $this->input->pipe($dest, $options);
}
public function write($data)
{
return $this->input->write($data);
}
public function end($data = null)
{
$this->input->end($data);
}
public function close()
{
$this->input->close();
$this->handleClose();
$this->removeAllListeners();
}
public function handleClose()
{
if (!\is_resource($this->stream)) {
return;
}
// Try to cleanly shut down socket and ignore any errors in case other
// side already closed. Underlying Stream implementation will take care
// of closing stream resource, so we otherwise keep this open here.
@\stream_socket_shutdown($this->stream, \STREAM_SHUT_RDWR);
}
public function getRemoteAddress()
{
if (!\is_resource($this->stream)) {
return null;
}
return $this->parseAddress(\stream_socket_get_name($this->stream, true));
}
public function getLocalAddress()
{
if (!\is_resource($this->stream)) {
return null;
}
return $this->parseAddress(\stream_socket_get_name($this->stream, false));
}
private function parseAddress($address)
{
if ($address === false) {
return null;
}
if ($this->unix) {
// remove trailing colon from address for HHVM < 3.19: https://3v4l.org/5C1lo
// note that technically ":" is a valid address, so keep this in place otherwise
if (\substr($address, -1) === ':' && \defined('HHVM_VERSION_ID') && \HHVM_VERSION_ID < 31900) {
$address = (string)\substr($address, 0, -1); // @codeCoverageIgnore
}
// work around unknown addresses should return null value: https://3v4l.org/5C1lo and https://bugs.php.net/bug.php?id=74556
// PHP uses "\0" string and HHVM uses empty string (colon removed above)
if ($address === '' || $address[0] === "\x00" ) {
return null; // @codeCoverageIgnore
}
return 'unix://' . $address;
}
// check if this is an IPv6 address which includes multiple colons but no square brackets
$pos = \strrpos($address, ':');
if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
$address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
}
return ($this->encryptionEnabled ? 'tls' : 'tcp') . '://' . $address;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace React\Socket;
use React\Stream\DuplexStreamInterface;
/**
* Any incoming and outgoing connection is represented by this interface,
* such as a normal TCP/IP connection.
*
* An incoming or outgoing connection is a duplex stream (both readable and
* writable) that implements React's
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
* It contains additional properties for the local and remote address (client IP)
* where this connection has been established to/from.
*
* Most commonly, instances implementing this `ConnectionInterface` are emitted
* by all classes implementing the [`ServerInterface`](#serverinterface) and
* used by all classes implementing the [`ConnectorInterface`](#connectorinterface).
*
* Because the `ConnectionInterface` implements the underlying
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface)
* you can use any of its events and methods as usual:
*
* ```php
* $connection->on('data', function ($chunk) {
* echo $chunk;
* });
*
* $connection->on('end', function () {
* echo 'ended';
* });
*
* $connection->on('error', function (Exception $e) {
* echo 'error: ' . $e->getMessage();
* });
*
* $connection->on('close', function () {
* echo 'closed';
* });
*
* $connection->write($data);
* $connection->end($data = null);
* $connection->close();
* // …
* ```
*
* For more details, see the
* [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface).
*
* @see DuplexStreamInterface
* @see ServerInterface
* @see ConnectorInterface
*/
interface ConnectionInterface extends DuplexStreamInterface
{
/**
* Returns the full remote address (URI) where this connection has been established with
*
* ```php
* $address = $connection->getRemoteAddress();
* echo 'Connection with ' . $address . PHP_EOL;
* ```
*
* If the remote address can not be determined or is unknown at this time (such as
* after the connection has been closed), it MAY return a `NULL` value instead.
*
* Otherwise, it will return the full address (URI) as a string value, such
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
* `unix://example.sock` or `unix:///path/to/example.sock`.
* Note that individual URI components are application specific and depend
* on the underlying transport protocol.
*
* If this is a TCP/IP based connection and you only want the remote IP, you may
* use something like this:
*
* ```php
* $address = $connection->getRemoteAddress();
* $ip = trim(parse_url($address, PHP_URL_HOST), '[]');
* echo 'Connection with ' . $ip . PHP_EOL;
* ```
*
* @return ?string remote address (URI) or null if unknown
*/
public function getRemoteAddress();
/**
* Returns the full local address (full URI with scheme, IP and port) where this connection has been established with
*
* ```php
* $address = $connection->getLocalAddress();
* echo 'Connection with ' . $address . PHP_EOL;
* ```
*
* If the local address can not be determined or is unknown at this time (such as
* after the connection has been closed), it MAY return a `NULL` value instead.
*
* Otherwise, it will return the full address (URI) as a string value, such
* as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`,
* `unix://example.sock` or `unix:///path/to/example.sock`.
* Note that individual URI components are application specific and depend
* on the underlying transport protocol.
*
* This method complements the [`getRemoteAddress()`](#getremoteaddress) method,
* so they should not be confused.
*
* If your `TcpServer` instance is listening on multiple interfaces (e.g. using
* the address `0.0.0.0`), you can use this method to find out which interface
* actually accepted this connection (such as a public or local interface).
*
* If your system has multiple interfaces (e.g. a WAN and a LAN interface),
* you can use this method to find out which interface was actually
* used for this connection.
*
* @return ?string local address (URI) or null if unknown
* @see self::getRemoteAddress()
*/
public function getLocalAddress();
}

236
vendor/react/socket/src/Connector.php vendored Normal file
View File

@@ -0,0 +1,236 @@
<?php
namespace React\Socket;
use React\Dns\Config\Config as DnsConfig;
use React\Dns\Resolver\Factory as DnsFactory;
use React\Dns\Resolver\ResolverInterface;
use React\EventLoop\LoopInterface;
/**
* The `Connector` class is the main class in this package that implements the
* `ConnectorInterface` and allows you to create streaming connections.
*
* You can use this connector to create any kind of streaming connections, such
* as plaintext TCP/IP, secure TLS or local Unix connection streams.
*
* Under the hood, the `Connector` is implemented as a *higher-level facade*
* for the lower-level connectors implemented in this package. This means it
* also shares all of their features and implementation details.
* If you want to typehint in your higher-level protocol implementation, you SHOULD
* use the generic [`ConnectorInterface`](#connectorinterface) instead.
*
* @see ConnectorInterface for the base interface
*/
final class Connector implements ConnectorInterface
{
private $connectors = array();
/**
* Instantiate new `Connector`
*
* ```php
* $connector = new React\Socket\Connector();
* ```
*
* This class takes two optional arguments for more advanced usage:
*
* ```php
* // constructor signature as of v1.9.0
* $connector = new React\Socket\Connector(array $context = [], ?LoopInterface $loop = null);
*
* // legacy constructor signature before v1.9.0
* $connector = new React\Socket\Connector(?LoopInterface $loop = null, array $context = []);
* ```
*
* 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 array|LoopInterface|null $context
* @param null|LoopInterface|array $loop
* @throws \InvalidArgumentException for invalid arguments
*/
public function __construct($context = array(), $loop = null)
{
// swap arguments for legacy constructor signature
if (($context instanceof LoopInterface || $context === null) && (\func_num_args() <= 1 || \is_array($loop))) {
$swap = $loop === null ? array(): $loop;
$loop = $context;
$context = $swap;
}
if (!\is_array($context) || ($loop !== null && !$loop instanceof LoopInterface)) {
throw new \InvalidArgumentException('Expected "array $context" and "?LoopInterface $loop" arguments');
}
// apply default options if not explicitly given
$context += array(
'tcp' => true,
'tls' => true,
'unix' => true,
'dns' => true,
'timeout' => true,
'happy_eyeballs' => true,
);
if ($context['timeout'] === true) {
$context['timeout'] = (float)\ini_get("default_socket_timeout");
}
if ($context['tcp'] instanceof ConnectorInterface) {
$tcp = $context['tcp'];
} else {
$tcp = new TcpConnector(
$loop,
\is_array($context['tcp']) ? $context['tcp'] : array()
);
}
if ($context['dns'] !== false) {
if ($context['dns'] instanceof ResolverInterface) {
$resolver = $context['dns'];
} else {
if ($context['dns'] !== true) {
$config = $context['dns'];
} else {
// try to load nameservers from system config or default to Google's public DNS
$config = DnsConfig::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8'; // @codeCoverageIgnore
}
}
$factory = new DnsFactory();
$resolver = $factory->createCached(
$config,
$loop
);
}
if ($context['happy_eyeballs'] === true) {
$tcp = new HappyEyeBallsConnector($loop, $tcp, $resolver);
} else {
$tcp = new DnsConnector($tcp, $resolver);
}
}
if ($context['tcp'] !== false) {
$context['tcp'] = $tcp;
if ($context['timeout'] !== false) {
$context['tcp'] = new TimeoutConnector(
$context['tcp'],
$context['timeout'],
$loop
);
}
$this->connectors['tcp'] = $context['tcp'];
}
if ($context['tls'] !== false) {
if (!$context['tls'] instanceof ConnectorInterface) {
$context['tls'] = new SecureConnector(
$tcp,
$loop,
\is_array($context['tls']) ? $context['tls'] : array()
);
}
if ($context['timeout'] !== false) {
$context['tls'] = new TimeoutConnector(
$context['tls'],
$context['timeout'],
$loop
);
}
$this->connectors['tls'] = $context['tls'];
}
if ($context['unix'] !== false) {
if (!$context['unix'] instanceof ConnectorInterface) {
$context['unix'] = new UnixConnector($loop);
}
$this->connectors['unix'] = $context['unix'];
}
}
public function connect($uri)
{
$scheme = 'tcp';
if (\strpos($uri, '://') !== false) {
$scheme = (string)\substr($uri, 0, \strpos($uri, '://'));
}
if (!isset($this->connectors[$scheme])) {
return \React\Promise\reject(new \RuntimeException(
'No connector available for URI scheme "' . $scheme . '" (EINVAL)',
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
));
}
return $this->connectors[$scheme]->connect($uri);
}
/**
* [internal] Builds on URI from the given URI parts and ip address with original hostname as query
*
* @param array $parts
* @param string $host
* @param string $ip
* @return string
* @internal
*/
public static function uri(array $parts, $host, $ip)
{
$uri = '';
// prepend original scheme if known
if (isset($parts['scheme'])) {
$uri .= $parts['scheme'] . '://';
}
if (\strpos($ip, ':') !== false) {
// enclose IPv6 addresses in square brackets before appending port
$uri .= '[' . $ip . ']';
} else {
$uri .= $ip;
}
// append original port if known
if (isset($parts['port'])) {
$uri .= ':' . $parts['port'];
}
// append orignal path if known
if (isset($parts['path'])) {
$uri .= $parts['path'];
}
// append original query if known
if (isset($parts['query'])) {
$uri .= '?' . $parts['query'];
}
// append original hostname as query if resolved via DNS and if
// destination URI does not contain "hostname" query param already
$args = array();
\parse_str(isset($parts['query']) ? $parts['query'] : '', $args);
if ($host !== $ip && !isset($args['hostname'])) {
$uri .= (isset($parts['query']) ? '&' : '?') . 'hostname=' . \rawurlencode($host);
}
// append original fragment if known
if (isset($parts['fragment'])) {
$uri .= '#' . $parts['fragment'];
}
return $uri;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace React\Socket;
/**
* The `ConnectorInterface` is responsible for providing an interface for
* establishing streaming connections, such as a normal TCP/IP connection.
*
* This is the main interface defined in this package and it is used throughout
* React's vast ecosystem.
*
* Most higher-level components (such as HTTP, database or other networking
* service clients) accept an instance implementing this interface to create their
* TCP/IP connection to the underlying networking service.
* This is usually done via dependency injection, so it's fairly simple to actually
* swap this implementation against any other implementation of this interface.
*
* The interface only offers a single `connect()` method.
*
* @see ConnectionInterface
*/
interface ConnectorInterface
{
/**
* Creates a streaming connection to the given remote address
*
* If returns a Promise which either fulfills with a stream implementing
* `ConnectionInterface` on success or rejects with an `Exception` if the
* connection is not successful.
*
* ```php
* $connector->connect('google.com:443')->then(
* function (React\Socket\ConnectionInterface $connection) {
* // connection successfully established
* },
* function (Exception $error) {
* // failed to connect due to $error
* }
* );
* ```
*
* The returned Promise MUST be implemented in such a way that it can be
* cancelled when it is still pending. Cancelling a pending promise MUST
* reject its value with an Exception. It SHOULD clean up any underlying
* resources and references as applicable.
*
* ```php
* $promise = $connector->connect($uri);
*
* $promise->cancel();
* ```
*
* @param string $uri
* @return \React\Promise\PromiseInterface<ConnectionInterface>
* Resolves with a `ConnectionInterface` on success or rejects with an `Exception` on error.
* @see ConnectionInterface
*/
public function connect($uri);
}

117
vendor/react/socket/src/DnsConnector.php vendored Normal file
View File

@@ -0,0 +1,117 @@
<?php
namespace React\Socket;
use React\Dns\Resolver\ResolverInterface;
use React\Promise;
use React\Promise\PromiseInterface;
final class DnsConnector implements ConnectorInterface
{
private $connector;
private $resolver;
public function __construct(ConnectorInterface $connector, ResolverInterface $resolver)
{
$this->connector = $connector;
$this->resolver = $resolver;
}
public function connect($uri)
{
$original = $uri;
if (\strpos($uri, '://') === false) {
$uri = 'tcp://' . $uri;
$parts = \parse_url($uri);
if (isset($parts['scheme'])) {
unset($parts['scheme']);
}
} else {
$parts = \parse_url($uri);
}
if (!$parts || !isset($parts['host'])) {
return Promise\reject(new \InvalidArgumentException(
'Given URI "' . $original . '" is invalid (EINVAL)',
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
));
}
$host = \trim($parts['host'], '[]');
$connector = $this->connector;
// skip DNS lookup / URI manipulation if this URI already contains an IP
if (@\inet_pton($host) !== false) {
return $connector->connect($original);
}
$promise = $this->resolver->resolve($host);
$resolved = null;
return new Promise\Promise(
function ($resolve, $reject) use (&$promise, &$resolved, $uri, $connector, $host, $parts) {
// resolve/reject with result of DNS lookup
$promise->then(function ($ip) use (&$promise, &$resolved, $uri, $connector, $host, $parts) {
$resolved = $ip;
return $promise = $connector->connect(
Connector::uri($parts, $host, $ip)
)->then(null, function (\Exception $e) use ($uri) {
if ($e instanceof \RuntimeException) {
$message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage());
$e = new \RuntimeException(
'Connection to ' . $uri . ' failed: ' . $message,
$e->getCode(),
$e
);
// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$r->setAccessible(true);
$trace = $r->getValue($e);
// Exception trace arguments are not available on some PHP 7.4 installs
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($e, $trace);
}
throw $e;
});
}, function ($e) use ($uri, $reject) {
$reject(new \RuntimeException('Connection to ' . $uri .' failed during DNS lookup: ' . $e->getMessage(), 0, $e));
})->then($resolve, $reject);
},
function ($_, $reject) use (&$promise, &$resolved, $uri) {
// cancellation should reject connection attempt
// reject DNS resolution with custom reason, otherwise rely on connection cancellation below
if ($resolved === null) {
$reject(new \RuntimeException(
'Connection to ' . $uri . ' cancelled during DNS lookup (ECONNABORTED)',
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
));
}
// (try to) cancel pending DNS lookup / connection attempt
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
// overwrite callback arguments for PHP7+ only, so they do not show
// up in the Exception trace and do not cause a possible cyclic reference.
$_ = $reject = null;
$promise->cancel();
$promise = null;
}
}
);
}
}

222
vendor/react/socket/src/FdServer.php vendored Normal file
View File

@@ -0,0 +1,222 @@
<?php
namespace React\Socket;
use Evenement\EventEmitter;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
/**
* [Internal] The `FdServer` class implements the `ServerInterface` and
* is responsible for accepting connections from an existing file descriptor.
*
* ```php
* $socket = new React\Socket\FdServer(3);
* ```
*
* Whenever a client connects, it will emit a `connection` event with a connection
* instance implementing `ConnectionInterface`:
*
* ```php
* $socket->on('connection', function (ConnectionInterface $connection) {
* echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* See also the `ServerInterface` for more details.
*
* @see ServerInterface
* @see ConnectionInterface
* @internal
*/
final class FdServer extends EventEmitter implements ServerInterface
{
private $master;
private $loop;
private $unix = false;
private $listening = false;
/**
* Creates a socket server and starts listening on the given file descriptor
*
* This starts accepting new incoming connections on the given file descriptor.
* See also the `connection event` documented in the `ServerInterface`
* for more details.
*
* ```php
* $socket = new React\Socket\FdServer(3);
* ```
*
* If the given FD is invalid or out of range, it will throw an `InvalidArgumentException`:
*
* ```php
* // throws InvalidArgumentException
* $socket = new React\Socket\FdServer(-1);
* ```
*
* If the given FD appears to be valid, but listening on it fails (such as
* if the FD does not exist or does not refer to a socket server), it will
* throw a `RuntimeException`:
*
* ```php
* // throws RuntimeException because FD does not reference a socket server
* $socket = new React\Socket\FdServer(0, $loop);
* ```
*
* Note that these error conditions may vary depending on your system and/or
* configuration.
* See the exception message and code for more details about the actual error
* condition.
*
* @param int|string $fd FD number such as `3` or as URL in the form of `php://fd/3`
* @param ?LoopInterface $loop
* @throws \InvalidArgumentException if the listening address is invalid
* @throws \RuntimeException if listening on this address fails (already in use etc.)
*/
public function __construct($fd, $loop = null)
{
if (\preg_match('#^php://fd/(\d+)$#', $fd, $m)) {
$fd = (int) $m[1];
}
if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) {
throw new \InvalidArgumentException(
'Invalid FD number given (EINVAL)',
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
);
}
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->loop = $loop ?: Loop::get();
$errno = 0;
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
// Match errstr from PHP's warning message.
// fopen(php://fd/3): Failed to open stream: Error duping file descriptor 3; possibly it doesn't exist: [9]: Bad file descriptor
\preg_match('/\[(\d+)\]: (.*)/', $error, $m);
$errno = isset($m[1]) ? (int) $m[1] : 0;
$errstr = isset($m[2]) ? $m[2] : $error;
});
$this->master = \fopen('php://fd/' . $fd, 'r+');
\restore_error_handler();
if (false === $this->master) {
throw new \RuntimeException(
'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno),
$errno
);
}
$meta = \stream_get_meta_data($this->master);
if (!isset($meta['stream_type']) || $meta['stream_type'] !== 'tcp_socket') {
\fclose($this->master);
$errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88;
$errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket';
throw new \RuntimeException(
'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (ENOTSOCK)',
$errno
);
}
// Socket should not have a peer address if this is a listening socket.
// Looks like this work-around is the closest we can get because PHP doesn't expose SO_ACCEPTCONN even with ext-sockets.
if (\stream_socket_get_name($this->master, true) !== false) {
\fclose($this->master);
$errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106;
$errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected';
throw new \RuntimeException(
'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (EISCONN)',
$errno
);
}
// Assume this is a Unix domain socket (UDS) when its listening address doesn't parse as a valid URL with a port.
// Looks like this work-around is the closest we can get because PHP doesn't expose SO_DOMAIN even with ext-sockets.
$this->unix = \parse_url($this->getAddress(), \PHP_URL_PORT) === false;
\stream_set_blocking($this->master, false);
$this->resume();
}
public function getAddress()
{
if (!\is_resource($this->master)) {
return null;
}
$address = \stream_socket_get_name($this->master, false);
if ($this->unix === true) {
return 'unix://' . $address;
}
// check if this is an IPv6 address which includes multiple colons but no square brackets
$pos = \strrpos($address, ':');
if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
$address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
}
return 'tcp://' . $address;
}
public function pause()
{
if (!$this->listening) {
return;
}
$this->loop->removeReadStream($this->master);
$this->listening = false;
}
public function resume()
{
if ($this->listening || !\is_resource($this->master)) {
return;
}
$that = $this;
$this->loop->addReadStream($this->master, function ($master) use ($that) {
try {
$newSocket = SocketServer::accept($master);
} catch (\RuntimeException $e) {
$that->emit('error', array($e));
return;
}
$that->handleConnection($newSocket);
});
$this->listening = true;
}
public function close()
{
if (!\is_resource($this->master)) {
return;
}
$this->pause();
\fclose($this->master);
$this->removeAllListeners();
}
/** @internal */
public function handleConnection($socket)
{
$connection = new Connection($socket, $this->loop);
$connection->unix = $this->unix;
$this->emit('connection', array($connection));
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace React\Socket;
/**
* Decorates an existing Connector to always use a fixed, preconfigured URI
*
* This can be useful for consumers that do not support certain URIs, such as
* when you want to explicitly connect to a Unix domain socket (UDS) path
* instead of connecting to a default address assumed by an higher-level API:
*
* ```php
* $connector = new React\Socket\FixedUriConnector(
* 'unix:///var/run/docker.sock',
* new React\Socket\UnixConnector()
* );
*
* // destination will be ignored, actually connects to Unix domain socket
* $promise = $connector->connect('localhost:80');
* ```
*/
class FixedUriConnector implements ConnectorInterface
{
private $uri;
private $connector;
/**
* @param string $uri
* @param ConnectorInterface $connector
*/
public function __construct($uri, ConnectorInterface $connector)
{
$this->uri = $uri;
$this->connector = $connector;
}
public function connect($_)
{
return $this->connector->connect($this->uri);
}
}

View File

@@ -0,0 +1,334 @@
<?php
namespace React\Socket;
use React\Dns\Model\Message;
use React\Dns\Resolver\ResolverInterface;
use React\EventLoop\LoopInterface;
use React\EventLoop\TimerInterface;
use React\Promise;
use React\Promise\PromiseInterface;
/**
* @internal
*/
final class HappyEyeBallsConnectionBuilder
{
/**
* As long as we haven't connected yet keep popping an IP address of the connect queue until one of them
* succeeds or they all fail. We will wait 100ms between connection attempts as per RFC.
*
* @link https://tools.ietf.org/html/rfc8305#section-5
*/
const CONNECTION_ATTEMPT_DELAY = 0.1;
/**
* Delay `A` lookup by 50ms sending out connection to IPv4 addresses when IPv6 records haven't
* resolved yet as per RFC.
*
* @link https://tools.ietf.org/html/rfc8305#section-3
*/
const RESOLUTION_DELAY = 0.05;
public $loop;
public $connector;
public $resolver;
public $uri;
public $host;
public $resolved = array(
Message::TYPE_A => false,
Message::TYPE_AAAA => false,
);
public $resolverPromises = array();
public $connectionPromises = array();
public $connectQueue = array();
public $nextAttemptTimer;
public $parts;
public $ipsCount = 0;
public $failureCount = 0;
public $resolve;
public $reject;
public $lastErrorFamily;
public $lastError6;
public $lastError4;
public function __construct(LoopInterface $loop, ConnectorInterface $connector, ResolverInterface $resolver, $uri, $host, $parts)
{
$this->loop = $loop;
$this->connector = $connector;
$this->resolver = $resolver;
$this->uri = $uri;
$this->host = $host;
$this->parts = $parts;
}
public function connect()
{
$that = $this;
return new Promise\Promise(function ($resolve, $reject) use ($that) {
$lookupResolve = function ($type) use ($that, $resolve, $reject) {
return function (array $ips) use ($that, $type, $resolve, $reject) {
unset($that->resolverPromises[$type]);
$that->resolved[$type] = true;
$that->mixIpsIntoConnectQueue($ips);
// start next connection attempt if not already awaiting next
if ($that->nextAttemptTimer === null && $that->connectQueue) {
$that->check($resolve, $reject);
}
};
};
$that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA));
$that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that) {
// happy path: IPv6 has resolved already (or could not resolve), continue with IPv4 addresses
if ($that->resolved[Message::TYPE_AAAA] === true || !$ips) {
return $ips;
}
// Otherwise delay processing IPv4 lookup until short timer passes or IPv6 resolves in the meantime
$deferred = new Promise\Deferred(function () use (&$ips) {
// discard all IPv4 addresses if cancelled
$ips = array();
});
$timer = $that->loop->addTimer($that::RESOLUTION_DELAY, function () use ($deferred, $ips) {
$deferred->resolve($ips);
});
$that->resolverPromises[Message::TYPE_AAAA]->then(function () use ($that, $timer, $deferred, &$ips) {
$that->loop->cancelTimer($timer);
$deferred->resolve($ips);
});
return $deferred->promise();
})->then($lookupResolve(Message::TYPE_A));
}, function ($_, $reject) use ($that) {
$reject(new \RuntimeException(
'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)',
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
));
$_ = $reject = null;
$that->cleanUp();
});
}
/**
* @internal
* @param int $type DNS query type
* @param callable $reject
* @return \React\Promise\PromiseInterface<string[]> Returns a promise that
* always resolves with a list of IP addresses on success or an empty
* list on error.
*/
public function resolve($type, $reject)
{
$that = $this;
return $that->resolver->resolveAll($that->host, $type)->then(null, function (\Exception $e) use ($type, $reject, $that) {
unset($that->resolverPromises[$type]);
$that->resolved[$type] = true;
if ($type === Message::TYPE_A) {
$that->lastError4 = $e->getMessage();
$that->lastErrorFamily = 4;
} else {
$that->lastError6 = $e->getMessage();
$that->lastErrorFamily = 6;
}
// cancel next attempt timer when there are no more IPs to connect to anymore
if ($that->nextAttemptTimer !== null && !$that->connectQueue) {
$that->loop->cancelTimer($that->nextAttemptTimer);
$that->nextAttemptTimer = null;
}
if ($that->hasBeenResolved() && $that->ipsCount === 0) {
$reject(new \RuntimeException(
$that->error(),
0,
$e
));
}
// Exception already handled above, so don't throw an unhandled rejection here
return array();
});
}
/**
* @internal
*/
public function check($resolve, $reject)
{
$ip = \array_shift($this->connectQueue);
// start connection attempt and remember array position to later unset again
$this->connectionPromises[] = $this->attemptConnection($ip);
\end($this->connectionPromises);
$index = \key($this->connectionPromises);
$that = $this;
$that->connectionPromises[$index]->then(function ($connection) use ($that, $index, $resolve) {
unset($that->connectionPromises[$index]);
$that->cleanUp();
$resolve($connection);
}, function (\Exception $e) use ($that, $index, $ip, $resolve, $reject) {
unset($that->connectionPromises[$index]);
$that->failureCount++;
$message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage());
if (\strpos($ip, ':') === false) {
$that->lastError4 = $message;
$that->lastErrorFamily = 4;
} else {
$that->lastError6 = $message;
$that->lastErrorFamily = 6;
}
// start next connection attempt immediately on error
if ($that->connectQueue) {
if ($that->nextAttemptTimer !== null) {
$that->loop->cancelTimer($that->nextAttemptTimer);
$that->nextAttemptTimer = null;
}
$that->check($resolve, $reject);
}
if ($that->hasBeenResolved() === false) {
return;
}
if ($that->ipsCount === $that->failureCount) {
$that->cleanUp();
$reject(new \RuntimeException(
$that->error(),
$e->getCode(),
$e
));
}
});
// Allow next connection attempt in 100ms: https://tools.ietf.org/html/rfc8305#section-5
// Only start timer when more IPs are queued or when DNS query is still pending (might add more IPs)
if ($this->nextAttemptTimer === null && (\count($this->connectQueue) > 0 || $this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) {
$this->nextAttemptTimer = $this->loop->addTimer(self::CONNECTION_ATTEMPT_DELAY, function () use ($that, $resolve, $reject) {
$that->nextAttemptTimer = null;
if ($that->connectQueue) {
$that->check($resolve, $reject);
}
});
}
}
/**
* @internal
*/
public function attemptConnection($ip)
{
$uri = Connector::uri($this->parts, $this->host, $ip);
return $this->connector->connect($uri);
}
/**
* @internal
*/
public function cleanUp()
{
// clear list of outstanding IPs to avoid creating new connections
$this->connectQueue = array();
// cancel pending connection attempts
foreach ($this->connectionPromises as $connectionPromise) {
if ($connectionPromise instanceof PromiseInterface && \method_exists($connectionPromise, 'cancel')) {
$connectionPromise->cancel();
}
}
// cancel pending DNS resolution (cancel IPv4 first in case it is awaiting IPv6 resolution delay)
foreach (\array_reverse($this->resolverPromises) as $resolverPromise) {
if ($resolverPromise instanceof PromiseInterface && \method_exists($resolverPromise, 'cancel')) {
$resolverPromise->cancel();
}
}
if ($this->nextAttemptTimer instanceof TimerInterface) {
$this->loop->cancelTimer($this->nextAttemptTimer);
$this->nextAttemptTimer = null;
}
}
/**
* @internal
*/
public function hasBeenResolved()
{
foreach ($this->resolved as $typeHasBeenResolved) {
if ($typeHasBeenResolved === false) {
return false;
}
}
return true;
}
/**
* Mixes an array of IP addresses into the connect queue in such a way they alternate when attempting to connect.
* The goal behind it is first attempt to connect to IPv6, then to IPv4, then to IPv6 again until one of those
* attempts succeeds.
*
* @link https://tools.ietf.org/html/rfc8305#section-4
*
* @internal
*/
public function mixIpsIntoConnectQueue(array $ips)
{
\shuffle($ips);
$this->ipsCount += \count($ips);
$connectQueueStash = $this->connectQueue;
$this->connectQueue = array();
while (\count($connectQueueStash) > 0 || \count($ips) > 0) {
if (\count($ips) > 0) {
$this->connectQueue[] = \array_shift($ips);
}
if (\count($connectQueueStash) > 0) {
$this->connectQueue[] = \array_shift($connectQueueStash);
}
}
}
/**
* @internal
* @return string
*/
public function error()
{
if ($this->lastError4 === $this->lastError6) {
$message = $this->lastError6;
} elseif ($this->lastErrorFamily === 6) {
$message = 'Last error for IPv6: ' . $this->lastError6 . '. Previous error for IPv4: ' . $this->lastError4;
} else {
$message = 'Last error for IPv4: ' . $this->lastError4 . '. Previous error for IPv6: ' . $this->lastError6;
}
if ($this->hasBeenResolved() && $this->ipsCount === 0) {
if ($this->lastError6 === $this->lastError4) {
$message = ' during DNS lookup: ' . $this->lastError6;
} else {
$message = ' during DNS lookup. ' . $message;
}
} else {
$message = ': ' . $message;
}
return 'Connection to ' . $this->uri . ' failed' . $message;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace React\Socket;
use React\Dns\Resolver\ResolverInterface;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise;
final class HappyEyeBallsConnector implements ConnectorInterface
{
private $loop;
private $connector;
private $resolver;
/**
* @param ?LoopInterface $loop
* @param ConnectorInterface $connector
* @param ResolverInterface $resolver
*/
public function __construct($loop = null, $connector = null, $resolver = null)
{
// $connector and $resolver arguments are actually required, marked
// optional for technical reasons only. Nullable $loop without default
// requires PHP 7.1, null default is also supported in legacy PHP
// versions, but required parameters are not allowed after arguments
// with null default. Mark all parameters optional and check accordingly.
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
}
if (!$connector instanceof ConnectorInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($connector) expected React\Socket\ConnectorInterface');
}
if (!$resolver instanceof ResolverInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #3 ($resolver) expected React\Dns\Resolver\ResolverInterface');
}
$this->loop = $loop ?: Loop::get();
$this->connector = $connector;
$this->resolver = $resolver;
}
public function connect($uri)
{
$original = $uri;
if (\strpos($uri, '://') === false) {
$uri = 'tcp://' . $uri;
$parts = \parse_url($uri);
if (isset($parts['scheme'])) {
unset($parts['scheme']);
}
} else {
$parts = \parse_url($uri);
}
if (!$parts || !isset($parts['host'])) {
return Promise\reject(new \InvalidArgumentException(
'Given URI "' . $original . '" is invalid (EINVAL)',
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
));
}
$host = \trim($parts['host'], '[]');
// skip DNS lookup / URI manipulation if this URI already contains an IP
if (@\inet_pton($host) !== false) {
return $this->connector->connect($original);
}
$builder = new HappyEyeBallsConnectionBuilder(
$this->loop,
$this->connector,
$this->resolver,
$uri,
$host,
$parts
);
return $builder->connect();
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace React\Socket;
use Evenement\EventEmitter;
use Exception;
use OverflowException;
/**
* The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible
* for limiting and keeping track of open connections to this server instance.
*
* Whenever the underlying server emits a `connection` event, it will check its
* limits and then either
* - keep track of this connection by adding it to the list of
* open connections and then forward the `connection` event
* - or reject (close) the connection when its limits are exceeded and will
* forward an `error` event instead.
*
* Whenever a connection closes, it will remove this connection from the list of
* open connections.
*
* ```php
* $server = new React\Socket\LimitingServer($server, 100);
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* See also the `ServerInterface` for more details.
*
* @see ServerInterface
* @see ConnectionInterface
*/
class LimitingServer extends EventEmitter implements ServerInterface
{
private $connections = array();
private $server;
private $limit;
private $pauseOnLimit = false;
private $autoPaused = false;
private $manuPaused = false;
/**
* Instantiates a new LimitingServer.
*
* You have to pass a maximum number of open connections to ensure
* the server will automatically reject (close) connections once this limit
* is exceeded. In this case, it will emit an `error` event to inform about
* this and no `connection` event will be emitted.
*
* ```php
* $server = new React\Socket\LimitingServer($server, 100);
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* You MAY pass a `null` limit in order to put no limit on the number of
* open connections and keep accepting new connection until you run out of
* operating system resources (such as open file handles). This may be
* useful if you do not want to take care of applying a limit but still want
* to use the `getConnections()` method.
*
* You can optionally configure the server to pause accepting new
* connections once the connection limit is reached. In this case, it will
* pause the underlying server and no longer process any new connections at
* all, thus also no longer closing any excessive connections.
* The underlying operating system is responsible for keeping a backlog of
* pending connections until its limit is reached, at which point it will
* start rejecting further connections.
* Once the server is below the connection limit, it will continue consuming
* connections from the backlog and will process any outstanding data on
* each connection.
* This mode may be useful for some protocols that are designed to wait for
* a response message (such as HTTP), but may be less useful for other
* protocols that demand immediate responses (such as a "welcome" message in
* an interactive chat).
*
* ```php
* $server = new React\Socket\LimitingServer($server, 100, true);
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* @param ServerInterface $server
* @param int|null $connectionLimit
* @param bool $pauseOnLimit
*/
public function __construct(ServerInterface $server, $connectionLimit, $pauseOnLimit = false)
{
$this->server = $server;
$this->limit = $connectionLimit;
if ($connectionLimit !== null) {
$this->pauseOnLimit = $pauseOnLimit;
}
$this->server->on('connection', array($this, 'handleConnection'));
$this->server->on('error', array($this, 'handleError'));
}
/**
* Returns an array with all currently active connections
*
* ```php
* foreach ($server->getConnection() as $connection) {
* $connection->write('Hi!');
* }
* ```
*
* @return ConnectionInterface[]
*/
public function getConnections()
{
return $this->connections;
}
public function getAddress()
{
return $this->server->getAddress();
}
public function pause()
{
if (!$this->manuPaused) {
$this->manuPaused = true;
if (!$this->autoPaused) {
$this->server->pause();
}
}
}
public function resume()
{
if ($this->manuPaused) {
$this->manuPaused = false;
if (!$this->autoPaused) {
$this->server->resume();
}
}
}
public function close()
{
$this->server->close();
}
/** @internal */
public function handleConnection(ConnectionInterface $connection)
{
// close connection if limit exceeded
if ($this->limit !== null && \count($this->connections) >= $this->limit) {
$this->handleError(new \OverflowException('Connection closed because server reached connection limit'));
$connection->close();
return;
}
$this->connections[] = $connection;
$that = $this;
$connection->on('close', function () use ($that, $connection) {
$that->handleDisconnection($connection);
});
// pause accepting new connections if limit exceeded
if ($this->pauseOnLimit && !$this->autoPaused && \count($this->connections) >= $this->limit) {
$this->autoPaused = true;
if (!$this->manuPaused) {
$this->server->pause();
}
}
$this->emit('connection', array($connection));
}
/** @internal */
public function handleDisconnection(ConnectionInterface $connection)
{
unset($this->connections[\array_search($connection, $this->connections)]);
// continue accepting new connection if below limit
if ($this->autoPaused && \count($this->connections) < $this->limit) {
$this->autoPaused = false;
if (!$this->manuPaused) {
$this->server->resume();
}
}
}
/** @internal */
public function handleError(\Exception $error)
{
$this->emit('error', array($error));
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace React\Socket;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise;
use BadMethodCallException;
use InvalidArgumentException;
use UnexpectedValueException;
final class SecureConnector implements ConnectorInterface
{
private $connector;
private $streamEncryption;
private $context;
/**
* @param ConnectorInterface $connector
* @param ?LoopInterface $loop
* @param array $context
*/
public function __construct(ConnectorInterface $connector, $loop = null, array $context = array())
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->connector = $connector;
$this->streamEncryption = new StreamEncryption($loop ?: Loop::get(), false);
$this->context = $context;
}
public function connect($uri)
{
if (!\function_exists('stream_socket_enable_crypto')) {
return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); // @codeCoverageIgnore
}
if (\strpos($uri, '://') === false) {
$uri = 'tls://' . $uri;
}
$parts = \parse_url($uri);
if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') {
return Promise\reject(new \InvalidArgumentException(
'Given URI "' . $uri . '" is invalid (EINVAL)',
\defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
));
}
$context = $this->context;
$encryption = $this->streamEncryption;
$connected = false;
/** @var \React\Promise\PromiseInterface<ConnectionInterface> $promise */
$promise = $this->connector->connect(
\str_replace('tls://', '', $uri)
)->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri, &$promise, &$connected) {
// (unencrypted) TCP/IP connection succeeded
$connected = true;
if (!$connection instanceof Connection) {
$connection->close();
throw new \UnexpectedValueException('Base connector does not use internal Connection class exposing stream resource');
}
// set required SSL/TLS context options
foreach ($context as $name => $value) {
\stream_context_set_option($connection->stream, 'ssl', $name, $value);
}
// try to enable encryption
return $promise = $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) {
// establishing encryption failed => close invalid connection and return error
$connection->close();
throw new \RuntimeException(
'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(),
$error->getCode()
);
});
}, function (\Exception $e) use ($uri) {
if ($e instanceof \RuntimeException) {
$message = \preg_replace('/^Connection to [^ ]+/', '', $e->getMessage());
$e = new \RuntimeException(
'Connection to ' . $uri . $message,
$e->getCode(),
$e
);
// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$r->setAccessible(true);
$trace = $r->getValue($e);
// Exception trace arguments are not available on some PHP 7.4 installs
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($e, $trace);
}
throw $e;
});
return new \React\Promise\Promise(
function ($resolve, $reject) use ($promise) {
$promise->then($resolve, $reject);
},
function ($_, $reject) use (&$promise, $uri, &$connected) {
if ($connected) {
$reject(new \RuntimeException(
'Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)',
\defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
));
}
$promise->cancel();
$promise = null;
}
);
}
}

210
vendor/react/socket/src/SecureServer.php vendored Normal file
View File

@@ -0,0 +1,210 @@
<?php
namespace React\Socket;
use Evenement\EventEmitter;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use BadMethodCallException;
use UnexpectedValueException;
/**
* The `SecureServer` class implements the `ServerInterface` and is responsible
* for providing a secure TLS (formerly known as SSL) server.
*
* It does so by wrapping a `TcpServer` instance which waits for plaintext
* TCP/IP connections and then performs a TLS handshake for each connection.
*
* ```php
* $server = new React\Socket\TcpServer(8000);
* $server = new React\Socket\SecureServer($server, null, array(
* // tls context options here…
* ));
* ```
*
* Whenever a client completes the TLS handshake, it will emit a `connection` event
* with a connection instance implementing [`ConnectionInterface`](#connectioninterface):
*
* ```php
* $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
* echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL;
*
* $connection->write('hello there!' . PHP_EOL);
* …
* });
* ```
*
* Whenever a client fails to perform a successful TLS handshake, it will emit an
* `error` event and then close the underlying TCP/IP connection:
*
* ```php
* $server->on('error', function (Exception $e) {
* echo 'Error' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* See also the `ServerInterface` for more details.
*
* Note that the `SecureServer` class is a concrete implementation for TLS sockets.
* If you want to typehint in your higher-level protocol implementation, you SHOULD
* use the generic `ServerInterface` instead.
*
* @see ServerInterface
* @see ConnectionInterface
*/
final class SecureServer extends EventEmitter implements ServerInterface
{
private $tcp;
private $encryption;
private $context;
/**
* Creates a secure TLS server and starts waiting for incoming connections
*
* It does so by wrapping a `TcpServer` instance which waits for plaintext
* TCP/IP connections and then performs a TLS handshake for each connection.
* It thus requires valid [TLS context options],
* which in its most basic form may look something like this if you're using a
* PEM encoded certificate file:
*
* ```php
* $server = new React\Socket\TcpServer(8000);
* $server = new React\Socket\SecureServer($server, null, array(
* 'local_cert' => 'server.pem'
* ));
* ```
*
* Note that the certificate file will not be loaded on instantiation but when an
* incoming connection initializes its TLS context.
* This implies that any invalid certificate file paths or contents will only cause
* an `error` event at a later time.
*
* If your private key is encrypted with a passphrase, you have to specify it
* like this:
*
* ```php
* $server = new React\Socket\TcpServer(8000);
* $server = new React\Socket\SecureServer($server, null, array(
* 'local_cert' => 'server.pem',
* 'passphrase' => 'secret'
* ));
* ```
*
* Note that available [TLS context options],
* their defaults and effects of changing these may vary depending on your system
* and/or PHP version.
* Passing unknown context options has no effect.
*
* 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.
*
* Advanced usage: Despite allowing any `ServerInterface` as first parameter,
* you SHOULD pass a `TcpServer` instance as first parameter, unless you
* know what you're doing.
* Internally, the `SecureServer` has to set the required TLS context options on
* the underlying stream resources.
* These resources are not exposed through any of the interfaces defined in this
* package, but only through the internal `Connection` class.
* The `TcpServer` class is guaranteed to emit connections that implement
* the `ConnectionInterface` and uses the internal `Connection` class in order to
* expose these underlying resources.
* If you use a custom `ServerInterface` and its `connection` event does not
* meet this requirement, the `SecureServer` will emit an `error` event and
* then close the underlying connection.
*
* @param ServerInterface|TcpServer $tcp
* @param ?LoopInterface $loop
* @param array $context
* @throws BadMethodCallException for legacy HHVM < 3.8 due to lack of support
* @see TcpServer
* @link https://www.php.net/manual/en/context.ssl.php for TLS context options
*/
public function __construct(ServerInterface $tcp, $loop = null, array $context = array())
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
if (!\function_exists('stream_socket_enable_crypto')) {
throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore
}
// default to empty passphrase to suppress blocking passphrase prompt
$context += array(
'passphrase' => ''
);
$this->tcp = $tcp;
$this->encryption = new StreamEncryption($loop ?: Loop::get());
$this->context = $context;
$that = $this;
$this->tcp->on('connection', function ($connection) use ($that) {
$that->handleConnection($connection);
});
$this->tcp->on('error', function ($error) use ($that) {
$that->emit('error', array($error));
});
}
public function getAddress()
{
$address = $this->tcp->getAddress();
if ($address === null) {
return null;
}
return \str_replace('tcp://' , 'tls://', $address);
}
public function pause()
{
$this->tcp->pause();
}
public function resume()
{
$this->tcp->resume();
}
public function close()
{
return $this->tcp->close();
}
/** @internal */
public function handleConnection(ConnectionInterface $connection)
{
if (!$connection instanceof Connection) {
$this->emit('error', array(new \UnexpectedValueException('Base server does not use internal Connection class exposing stream resource')));
$connection->close();
return;
}
foreach ($this->context as $name => $value) {
\stream_context_set_option($connection->stream, 'ssl', $name, $value);
}
// get remote address before starting TLS handshake in case connection closes during handshake
$remote = $connection->getRemoteAddress();
$that = $this;
$this->encryption->enable($connection)->then(
function ($conn) use ($that) {
$that->emit('connection', array($conn));
},
function ($error) use ($that, $connection, $remote) {
$error = new \RuntimeException(
'Connection from ' . $remote . ' failed during TLS handshake: ' . $error->getMessage(),
$error->getCode()
);
$that->emit('error', array($error));
$connection->close();
}
);
}
}

Some files were not shown because too many files have changed in this diff Show More