Version v0.12.0-dev-2

This commit is contained in:
raviks789 2023-06-26 10:02:04 +00:00
parent adb56cbe0a
commit 2a440f11f4
4749 changed files with 1484894 additions and 0 deletions

1
VERSION Normal file
View File

@ -0,0 +1 @@
v0.12.0-dev-2

10881
asset/js/jquery/jquery.js vendored Normal file

File diff suppressed because it is too large Load Diff

2
asset/js/jquery/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3955
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

25
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInitcb685cfd85103a7392235f95584fc4f5::getLoader();

119
vendor/bin/lessc vendored Executable file
View File

@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
/**
* Proxy PHP file generated by Composer
*
* This file includes the referenced bin path (../wikimedia/less.php/bin/lessc)
* using a stream wrapper to prevent the shebang from being output on PHP<8
*
* @generated
*/
namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) {
if (!class_exists('Composer\BinProxyWrapper')) {
/**
* @internal
*/
final class BinProxyWrapper
{
private $handle;
private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path)
{
// get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 17);
$this->realpath = realpath($opened_path) ?: $opened_path;
$opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0;
return (bool) $this->handle;
}
public function stream_read($count)
{
$data = fread($this->handle, $count);
if ($this->position === 0) {
$data = preg_replace('{^#!.*\r?\n}', '', $data);
}
$this->position += strlen($data);
return $data;
}
public function stream_cast($castAs)
{
return $this->handle;
}
public function stream_close()
{
fclose($this->handle);
}
public function stream_lock($operation)
{
return $operation ? flock($this->handle, $operation) : true;
}
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell()
{
return $this->position;
}
public function stream_eof()
{
return feof($this->handle);
}
public function stream_stat()
{
return array();
}
public function stream_set_option($option, $arg1, $arg2)
{
return true;
}
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
}
}
if (
(function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
|| (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/wikimedia/less.php/bin/lessc');
}
}
return include __DIR__ . '/..'.'/wikimedia/less.php/bin/lessc';

152
vendor/clue/block-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,152 @@
# Changelog
## 1.5.0 (2021-10-20)
* Feature: Simplify usage by supporting new [default loop](https://github.com/reactphp/event-loop#loop).
(#60 by @clue)
```php
// old (still supported)
Clue\React\Block\await($promise, $loop);
Clue\React\Block\awaitAny($promises, $loop);
Clue\React\Block\awaitAll($promises, $loop);
// new (using default loop)
Clue\React\Block\await($promise);
Clue\React\Block\awaitAny($promises);
Clue\React\Block\awaitAll($promises);
```
* Feature: Added support for upcoming react/promise v3.
(#61 by @davidcole1340 and @SimonFrings)
* Improve error reporting by appending previous message for `Throwable`s.
(#57 by @clue)
* Deprecate `$timeout` argument for `await*()` functions.
(#59 by @clue)
```php
// deprecated
Clue\React\Block\await($promise, $loop, $timeout);
Clue\React\Block\awaitAny($promises, $loop, $timeout);
Clue\React\Block\awaitAll($promises, $loop, $timeout);
// still supported
Clue\React\Block\await($promise, $loop);
Clue\React\Block\awaitAny($promises, $loop);
Clue\React\Block\awaitAll($promises, $loop);
```
* Improve API documentation.
(#58 and #63 by @clue and #55 by @PaulRotmann)
* Improve test suite and use GitHub actions for continuous integration (CI).
(#54 by @SimonFrings)
## 1.4.0 (2020-08-21)
* Improve API documentation, update README and add examples.
(#45 by @clue and #51 by @SimonFrings)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Prepare PHP 8 support, update to PHPUnit 9, run tests on PHP 7.4 and simplify test matrix.
(#46, #47 and #50 by @SimonFrings)
## 1.3.1 (2019-04-09)
* Fix: Fix getting the type of unexpected rejection reason when not rejecting with an `Exception`.
(#42 by @Furgas and @clue)
* Fix: Check if the function is declared before declaring it.
(#39 by @Niko9911)
## 1.3.0 (2018-06-14)
* Feature: Improve memory consumption by cleaning up garbage references.
(#35 by @clue)
* Fix minor documentation typos.
(#28 by @seregazhuk)
* Improve test suite by locking Travis distro so new defaults will not break the build,
support PHPUnit 6 and update Travis config to also test against PHP 7.2.
(#30 by @clue, #31 by @carusogabriel and #32 by @andreybolonin)
* Update project homepage.
(#34 by @clue)
## 1.2.0 (2017-08-03)
* Feature / Fix: Forward compatibility with future EventLoop v1.0 and v0.5 and
cap small timeout values for legacy EventLoop
(#26 by @clue)
```php
// now works across all versions
Block\sleep(0.000001, $loop);
```
* Feature / Fix: Throw `UnexpectedValueException` if Promise gets rejected with non-Exception
(#27 by @clue)
```php
// now throws an UnexceptedValueException
Block\await(Promise\reject(false), $loop);
```
* First class support for legacy PHP 5.3 through PHP 7.1 and HHVM
(#24 and #25 by @clue)
* Improve testsuite by adding PHPUnit to require-dev and
Fix HHVM build for now again and ignore future HHVM build errors
(#23 and #24 by @clue)
## 1.1.0 (2016-03-09)
* Feature: Add optional timeout parameter to all await*() functions
(#17 by @clue)
* Feature: Cancellation is now supported across all PHP versions
(#16 by @clue)
## 1.0.0 (2015-11-13)
* First stable release, now following SemVer
* Improved documentation
> Contains no other changes, so it's actually fully compatible with the v0.3.0 release.
## 0.3.0 (2015-07-09)
* BC break: Use functional API approach instead of pseudo-OOP.
All existing methods are now exposed as simple functions.
([#13](https://github.com/clue/php-block-react/pull/13))
```php
// old
$blocker = new Block\Blocker($loop);
$result = $blocker->await($promise);
// new
$result = Block\await($promise, $loop);
```
## 0.2.0 (2015-07-05)
* BC break: Rename methods in order to avoid confusion.
* Rename `wait()` to `sleep()`.
([#8](https://github.com/clue/php-block-react/pull/8))
* Rename `awaitRace()` to `awaitAny()`.
([#9](https://github.com/clue/php-block-react/pull/9))
* Rename `awaitOne()` to `await()`.
([#10](https://github.com/clue/php-block-react/pull/10))
## 0.1.1 (2015-04-05)
* `run()` the loop instead of making it `tick()`.
This results in significant performance improvements (less resource utilization) by avoiding busy waiting
([#1](https://github.com/clue/php-block-react/pull/1))
## 0.1.0 (2015-04-04)
* First tagged release

21
vendor/clue/block-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Christian Lück
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.

335
vendor/clue/block-react/README.md vendored Normal file
View File

@ -0,0 +1,335 @@
# clue/reactphp-block
[![CI status](https://github.com/clue/reactphp-block/workflows/CI/badge.svg)](https://github.com/clue/reactphp-block/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/block-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/block-react)
Lightweight library that eases integrating async components built for
[ReactPHP](https://reactphp.org/) in a traditional, blocking environment.
[ReactPHP](https://reactphp.org/) provides you a great set of base components and
a huge ecosystem of third party libraries in order to perform async operations.
The event-driven paradigm and asynchronous processing of any number of streams
in real time enables you to build a whole new set of application on top of it.
This is great for building modern, scalable applications from scratch and will
likely result in you relying on a whole new software architecture.
But let's face it: Your day-to-day business is unlikely to allow you to build
everything from scratch and ditch your existing production environment.
This is where this library comes into play:
*Let's block ReactPHP*
More specifically, this library eases the pain of integrating async components
into your traditional, synchronous (blocking) application stack.
**Table of contents**
* [Support us](#support-us)
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [sleep()](#sleep)
* [await()](#await)
* [awaitAny()](#awaitany)
* [awaitAll()](#awaitall)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
### Quickstart example
The following example code demonstrates how this library can be used along with
an [async HTTP client](https://github.com/reactphp/http#client-usage) to process two
non-blocking HTTP requests and block until the first (faster) one resolves.
```php
function blockingExample()
{
// this example uses an HTTP client
// this could be pretty much everything that binds to an event loop
$browser = new React\Http\Browser();
// set up two parallel requests
$request1 = $browser->get('http://www.google.com/');
$request2 = $browser->get('http://www.google.co.uk/');
// keep the loop running (i.e. block) until the first response arrives
$fasterResponse = Clue\React\Block\awaitAny(array($request1, $request2));
return $fasterResponse->getBody();
}
```
## Usage
This lightweight library consists only of a few simple functions.
All functions reside under the `Clue\React\Block` namespace.
The below examples refer to all functions with their fully-qualified names like this:
```php
Clue\React\Block\await(…);
```
As of PHP 5.6+ you can also import each required function into your code like this:
```php
use function Clue\React\Block\await;
await(…);
```
Alternatively, you can also use an import statement similar to this:
```php
use Clue\React\Block;
Block\await(…);
```
### sleep()
The `sleep(float $seconds, ?LoopInterface $loop = null): void` function can be used to
wait/sleep for `$time` seconds.
```php
Clue\React\Block\sleep(1.5, $loop);
```
This function will only return after the given `$time` has elapsed. In the
meantime, the event loop will run any other events attached to the same loop
until the timer fires. If there are no other events attached to this loop,
it will behave similar to the built-in [`sleep()`](https://www.php.net/manual/en/function.sleep.php).
Internally, the `$time` 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.
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
pass the event loop instance to use. 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.
Note that this function will assume control over the event loop. Internally, it
will actually `run()` the loop until the timer fires and then calls `stop()` to
terminate execution of the loop. This means this function is more suited for
short-lived program executions when using async APIs is not feasible. For
long-running applications, using event-driven APIs by leveraging timers
is usually preferable.
### await()
The `await(PromiseInterface $promise, ?LoopInterface $loop = null, ?float $timeout = null): mixed` function can be used to
block waiting for the given `$promise` to be fulfilled.
```php
$result = Clue\React\Block\await($promise);
```
This function will only return after the given `$promise` has settled, i.e.
either fulfilled or rejected. In the meantime, the event loop will run any
events attached to the same loop until the promise settles.
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`, then this function
will throw an `UnexpectedValueException` instead.
```php
try {
$result = Clue\React\Block\await($promise);
// promise successfully fulfilled with $result
echo 'Result: ' . $result;
} catch (Exception $exception) {
// promise rejected with $exception
echo 'ERROR: ' . $exception->getMessage();
}
```
See also the [examples](examples/).
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
pass the event loop instance to use. 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.
If no `$timeout` argument is given and the promise stays pending, then this
will potentially wait/block forever until the promise is settled. To avoid
this, API authors creating promises are expected to provide means to
configure a timeout for the promise instead. For more details, see also the
[`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
If the deprecated `$timeout` argument is given and the promise is still pending once the
timeout triggers, this will `cancel()` the promise and throw a `TimeoutException`.
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.
Note that this function will assume control over the event loop. Internally, it
will actually `run()` the loop until the promise settles and then calls `stop()` to
terminate execution of the loop. This means this function is more suited for
short-lived promise executions when using promise-based APIs is not feasible.
For long-running applications, using promise-based APIs by leveraging chained
`then()` calls is usually preferable.
### awaitAny()
The `awaitAny(PromiseInterface[] $promises, ?LoopInterface $loop = null, ?float $timeout = null): mixed` function can be used to
wait for ANY of the given promises to be fulfilled.
```php
$promises = array(
$promise1,
$promise2
);
$firstResult = Clue\React\Block\awaitAny($promises);
echo 'First result: ' . $firstResult;
```
See also the [examples](examples/).
This function will only return after ANY of the given `$promises` has been
fulfilled or will throw when ALL of them have been rejected. In the meantime,
the event loop will run any events attached to the same loop.
Once ANY promise is fulfilled, this function will return whatever this
promise resolved to and will try to `cancel()` all remaining promises.
Once ALL promises reject, this function will fail and throw an `UnderflowException`.
Likewise, this will throw if an empty array of `$promises` is passed.
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
pass the event loop instance to use. 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.
If no `$timeout` argument is given and ALL promises stay pending, then this
will potentially wait/block forever until the promise is fulfilled. To avoid
this, API authors creating promises are expected to provide means to
configure a timeout for the promise instead. For more details, see also the
[`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
If the deprecated `$timeout` argument is given and ANY promises are still pending once
the timeout triggers, this will `cancel()` all pending promises and throw a
`TimeoutException`. 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.
Note that this function will assume control over the event loop. Internally, it
will actually `run()` the loop until the promise settles and then calls `stop()` to
terminate execution of the loop. This means this function is more suited for
short-lived promise executions when using promise-based APIs is not feasible.
For long-running applications, using promise-based APIs by leveraging chained
`then()` calls is usually preferable.
### awaitAll()
The `awaitAll(PromiseInterface[] $promises, ?LoopInterface $loop = null, ?float $timeout = null): mixed[]` function can be used to
wait for ALL of the given promises to be fulfilled.
```php
$promises = array(
$promise1,
$promise2
);
$allResults = Clue\React\Block\awaitAll($promises);
echo 'First promise resolved with: ' . $allResults[0];
```
See also the [examples](examples/).
This function will only return after ALL of the given `$promises` have been
fulfilled or will throw when ANY of them have been rejected. In the meantime,
the event loop will run any events attached to the same loop.
Once ALL promises are fulfilled, this will return an array with whatever
each promise resolves to. Array keys will be left intact, i.e. they can
be used to correlate the return array to the promises passed.
Likewise, this will return an empty array if an empty array of `$promises` is passed.
Once ANY promise rejects, this will try to `cancel()` all remaining promises
and throw an `Exception`. If the promise did not reject with an `Exception`,
then this function will throw an `UnexpectedValueException` instead.
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
pass the event loop instance to use. 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.
If no `$timeout` argument is given and ANY promises stay pending, then this
will potentially wait/block forever until the promise is fulfilled. To avoid
this, API authors creating promises are expected to provide means to
configure a timeout for the promise instead. For more details, see also the
[`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
If the deprecated `$timeout` argument is given and ANY promises are still pending once
the timeout triggers, this will `cancel()` all pending promises and throw a
`TimeoutException`. 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.
Note that this function will assume control over the event loop. Internally, it
will actually `run()` the loop until the promise settles and then calls `stop()` to
terminate execution of the loop. This means this function is more suited for
short-lived promise executions when using promise-based APIs is not feasible.
For long-running applications, using promise-based APIs by leveraging chained
`then()` calls is usually preferable.
## 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 clue/block-react:^1.5
```
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
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

29
vendor/clue/block-react/composer.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
"name": "clue/block-react",
"description": "Lightweight library that eases integrating async components built for ReactPHP in a traditional, blocking environment.",
"keywords": ["blocking", "await", "sleep", "Event Loop", "synchronous", "Promise", "ReactPHP", "async"],
"homepage": "https://github.com/clue/reactphp-block",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"autoload": {
"files": [ "src/functions_include.php" ]
},
"autoload-dev": {
"psr-4": { "Clue\\Tests\\React\\Block\\": "tests/" }
},
"require": {
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3.0 || ^2.7 || ^1.2.1",
"react/promise-timer": "^1.5"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
"react/http": "^1.4"
}
}

View File

@ -0,0 +1,357 @@
<?php
namespace Clue\React\Block;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise;
use React\Promise\CancellablePromiseInterface;
use React\Promise\PromiseInterface;
use React\Promise\Timer;
use React\Promise\Timer\TimeoutException;
use Exception;
use UnderflowException;
/**
* Wait/sleep for `$time` seconds.
*
* ```php
* Clue\React\Block\sleep(1.5, $loop);
* ```
*
* This function will only return after the given `$time` has elapsed. In the
* meantime, the event loop will run any other events attached to the same loop
* until the timer fires. If there are no other events attached to this loop,
* it will behave similar to the built-in [`sleep()`](https://www.php.net/manual/en/function.sleep.php).
*
* Internally, the `$time` 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.
*
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
* pass the event loop instance to use. 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.
*
* Note that this function will assume control over the event loop. Internally, it
* will actually `run()` the loop until the timer fires and then calls `stop()` to
* terminate execution of the loop. This means this function is more suited for
* short-lived program executions when using async APIs is not feasible. For
* long-running applications, using event-driven APIs by leveraging timers
* is usually preferable.
*
* @param float $time
* @param ?LoopInterface $loop
* @return void
*/
function sleep($time, LoopInterface $loop = null)
{
await(Timer\resolve($time, $loop), $loop);
}
/**
* Block waiting for the given `$promise` to be fulfilled.
*
* ```php
* $result = Clue\React\Block\await($promise, $loop);
* ```
*
* This function will only return after the given `$promise` has settled, i.e.
* either fulfilled or rejected. In the meantime, the event loop will run any
* events attached to the same loop until the promise settles.
*
* 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`, then this function
* will throw an `UnexpectedValueException` instead.
*
* ```php
* try {
* $result = Clue\React\Block\await($promise, $loop);
* // promise successfully fulfilled with $result
* echo 'Result: ' . $result;
* } catch (Exception $exception) {
* // promise rejected with $exception
* echo 'ERROR: ' . $exception->getMessage();
* }
* ```
*
* See also the [examples](../examples/).
*
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
* pass the event loop instance to use. 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.
*
* If no `$timeout` argument is given and the promise stays pending, then this
* will potentially wait/block forever until the promise is settled. To avoid
* this, API authors creating promises are expected to provide means to
* configure a timeout for the promise instead. For more details, see also the
* [`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
*
* If the deprecated `$timeout` argument is given and the promise is still pending once the
* timeout triggers, this will `cancel()` the promise and throw a `TimeoutException`.
* 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.
*
* Note that this function will assume control over the event loop. Internally, it
* will actually `run()` the loop until the promise settles and then calls `stop()` to
* terminate execution of the loop. This means this function is more suited for
* short-lived promise executions when using promise-based APIs is not feasible.
* For long-running applications, using promise-based APIs by leveraging chained
* `then()` calls is usually preferable.
*
* @param PromiseInterface $promise
* @param ?LoopInterface $loop
* @param ?float $timeout [deprecated] (optional) maximum timeout in seconds or null=wait forever
* @return mixed returns whatever the promise resolves to
* @throws Exception when the promise is rejected
* @throws TimeoutException if the $timeout is given and triggers
*/
function await(PromiseInterface $promise, LoopInterface $loop = null, $timeout = null)
{
$wait = true;
$resolved = null;
$exception = null;
$rejected = false;
$loop = $loop ?: Loop::get();
if ($timeout !== null) {
$promise = Timer\timeout($promise, $timeout, $loop);
}
$promise->then(
function ($c) use (&$resolved, &$wait, $loop) {
$resolved = $c;
$wait = false;
$loop->stop();
},
function ($error) use (&$exception, &$rejected, &$wait, $loop) {
$exception = $error;
$rejected = true;
$wait = false;
$loop->stop();
}
);
// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$promise = null;
while ($wait) {
$loop->run();
}
if ($rejected) {
if (!$exception instanceof \Exception && !$exception instanceof \Throwable) {
$exception = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
);
} elseif (!$exception instanceof \Exception) {
$exception = new \UnexpectedValueException(
'Promise rejected with unexpected ' . get_class($exception) . ': ' . $exception->getMessage(),
$exception->getCode(),
$exception
);
}
throw $exception;
}
return $resolved;
}
/**
* Wait for ANY of the given promises to be fulfilled.
*
* ```php
* $promises = array(
* $promise1,
* $promise2
* );
*
* $firstResult = Clue\React\Block\awaitAny($promises, $loop);
*
* echo 'First result: ' . $firstResult;
* ```
*
* See also the [examples](../examples/).
*
* This function will only return after ANY of the given `$promises` has been
* fulfilled or will throw when ALL of them have been rejected. In the meantime,
* the event loop will run any events attached to the same loop.
*
* Once ANY promise is fulfilled, this function will return whatever this
* promise resolved to and will try to `cancel()` all remaining promises.
*
* Once ALL promises reject, this function will fail and throw an `UnderflowException`.
* Likewise, this will throw if an empty array of `$promises` is passed.
*
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
* pass the event loop instance to use. 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.
*
* If no `$timeout` argument is given and ALL promises stay pending, then this
* will potentially wait/block forever until the promise is fulfilled. To avoid
* this, API authors creating promises are expected to provide means to
* configure a timeout for the promise instead. For more details, see also the
* [`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
*
* If the deprecated `$timeout` argument is given and ANY promises are still pending once
* the timeout triggers, this will `cancel()` all pending promises and throw a
* `TimeoutException`. 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.
*
* Note that this function will assume control over the event loop. Internally, it
* will actually `run()` the loop until the promise settles and then calls `stop()` to
* terminate execution of the loop. This means this function is more suited for
* short-lived promise executions when using promise-based APIs is not feasible.
* For long-running applications, using promise-based APIs by leveraging chained
* `then()` calls is usually preferable.
*
* @param PromiseInterface[] $promises
* @param ?LoopInterface $loop
* @param ?float $timeout [deprecated] (optional) maximum timeout in seconds or null=wait forever
* @return mixed returns whatever the first promise resolves to
* @throws Exception if ALL promises are rejected
* @throws TimeoutException if the $timeout is given and triggers
*/
function awaitAny(array $promises, LoopInterface $loop = null, $timeout = null)
{
// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$all = $promises;
$promises = null;
try {
// Promise\any() does not cope with an empty input array, so reject this here
if (!$all) {
throw new UnderflowException('Empty input array');
}
$ret = await(Promise\any($all)->then(null, function () {
// rejects with an array of rejection reasons => reject with Exception instead
throw new Exception('All promises rejected');
}), $loop, $timeout);
} catch (TimeoutException $e) {
// the timeout fired
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($all);
throw $e;
} catch (Exception $e) {
// if the above throws, then ALL promises are already rejected
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($all);
throw new UnderflowException('No promise could resolve', 0, $e);
}
// if we reach this, then ANY of the given promises resolved
// => try to cancel all promises (settled ones will be ignored anyway)
_cancelAllPromises($all);
return $ret;
}
/**
* Wait for ALL of the given promises to be fulfilled.
*
* ```php
* $promises = array(
* $promise1,
* $promise2
* );
*
* $allResults = Clue\React\Block\awaitAll($promises, $loop);
*
* echo 'First promise resolved with: ' . $allResults[0];
* ```
*
* See also the [examples](../examples/).
*
* This function will only return after ALL of the given `$promises` have been
* fulfilled or will throw when ANY of them have been rejected. In the meantime,
* the event loop will run any events attached to the same loop.
*
* Once ALL promises are fulfilled, this will return an array with whatever
* each promise resolves to. Array keys will be left intact, i.e. they can
* be used to correlate the return array to the promises passed.
*
* Once ANY promise rejects, this will try to `cancel()` all remaining promises
* and throw an `Exception`. If the promise did not reject with an `Exception`,
* then this function will throw an `UnexpectedValueException` instead.
*
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
* pass the event loop instance to use. 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.
*
* If no `$timeout` argument is given and ANY promises stay pending, then this
* will potentially wait/block forever until the promise is fulfilled. To avoid
* this, API authors creating promises are expected to provide means to
* configure a timeout for the promise instead. For more details, see also the
* [`timeout()` function](https://github.com/reactphp/promise-timer#timeout).
*
* If the deprecated `$timeout` argument is given and ANY promises are still pending once
* the timeout triggers, this will `cancel()` all pending promises and throw a
* `TimeoutException`. 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.
*
* Note that this function will assume control over the event loop. Internally, it
* will actually `run()` the loop until the promise settles and then calls `stop()` to
* terminate execution of the loop. This means this function is more suited for
* short-lived promise executions when using promise-based APIs is not feasible.
* For long-running applications, using promise-based APIs by leveraging chained
* `then()` calls is usually preferable.
*
* @param PromiseInterface[] $promises
* @param ?LoopInterface $loop
* @param ?float $timeout [deprecated] (optional) maximum timeout in seconds or null=wait forever
* @return array returns an array with whatever each promise resolves to
* @throws Exception when ANY promise is rejected
* @throws TimeoutException if the $timeout is given and triggers
*/
function awaitAll(array $promises, LoopInterface $loop = null, $timeout = null)
{
// Explicitly overwrite argument with null value. This ensure that this
// argument does not show up in the stack trace in PHP 7+ only.
$all = $promises;
$promises = null;
try {
return await(Promise\all($all), $loop, $timeout);
} catch (Exception $e) {
// ANY of the given promises rejected or the timeout fired
// => try to cancel all promises (rejected ones will be ignored anyway)
_cancelAllPromises($all);
throw $e;
}
}
/**
* internal helper function used to iterate over an array of Promise instances and cancel() each
*
* @internal
* @param array $promises
* @return void
*/
function _cancelAllPromises(array $promises)
{
foreach ($promises as $promise) {
if ($promise instanceof PromiseInterface && ($promise instanceof CancellablePromiseInterface || !\interface_exists('React\Promise\CancellablePromiseInterface'))) {
$promise->cancel();
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Clue\React\Block;
if (!function_exists('Clue\\React\\Block\\sleep')) {
require __DIR__ . '/functions.php';
}

522
vendor/clue/buzz-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,522 @@
# Changelog
## 2.9.0 (2020-07-03)
A **major feature release** adding a new options APIs and more consistent APIs
for sending streaming requests. Includes a major documentation overhaul and
deprecates a number of APIs.
* Feature: Add new `request()` and `requestStreaming()` methods and
deprecate `send()` method and `streaming` option.
(#170 by @clue)
```php
// old: deprecated
$browser->withOptions(['streaming' => true])->get($url);
$browser->send(new Request('OPTIONS', $url));
// new
$browser->requestStreaming('GET', $url);
$browser->request('OPTIONS', $url);
```
* Feature: Add dedicated methods to control options, deprecate `withOptions()`.
(#172 by @clue)
```php
// old: deprecated
$browser->withOptions(['timeout' => 10]);
$browser->withOptions(['followRedirects' => false]);
$browser->withOptions(['obeySuccessCode' => false]);
// new
$browser->withTimeout(10);
$browser->withFollowRedirects(false);
$browser->withRejectErrorResponse(false);
```
* Feature: Add `withResponseBuffer()` method to limit maximum response buffer size (defaults to 16 MiB).
(#175 by @clue)
```php
// new: download maximum of 100 MB
$browser->withResponseBuffer(100 * 1000000)->get($url);
```
* Feature: Improve `withBase()` method and deprecate `withoutBase()` method
(#173 by @clue)
```php
// old: deprecated
$browser = $browser->withoutBase();
// new
$browser = $browser->withBase(null);
```
* Deprecate `submit()` method, use `post()` instead.
(#171 by @clue)
```php
// old: deprecated
$browser->submit($url, $data);
// new
$browser->post($url, ['Content-Type' => 'application/x-www-form-urlencoded'], http_build_query($data));
```
* Deprecate `UriInterface` for request methods, use URL strings instead
(#174 by @clue)
* Fix: Fix unneeded timeout timer when request body closes and sender already rejected.
(#169 by @clue)
* Improve documentation structure, add documentation for all API methods and
handling concurrency.
(#167 and #176 by @clue)
* Improve test suite to use ReactPHP-based webserver instead of httpbin and
add forward compatibility with PHPUnit 9.
(#168 by @clue)
## 2.8.2 (2020-06-02)
* Fix: HTTP `HEAD` requests should not expect a response body.
(#166 by @clue)
## 2.8.1 (2020-05-19)
* Fix: Fix cancellation of pending requests with promise followers.
(#164 by @clue)
## 2.8.0 (2020-05-13)
* Feature: Use HTTP/1.1 protocol version by default and add new `Browser::withProtocolVersion()`.
(#162 by @clue)
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. You can revert
to legacy HTTP/1.0 like this:
```php
$browser->withProtocolVersion('1.0')->get($url)->then(…);
```
* Feature / Fix: Explicitly close connection after response body ends.
(#161 by @clue)
This improves support for servers ignoring the `Connection: close` request
header that would otherwise keep the connection open and could eventually
run into a timeout even though the transfer was completed.
* Fixed small issue in code example.
(#160 by @mmoreram)
* Clean up test suite and add `.gitattributes` to exclude dev files from exports.
(#163 by @SimonFrings)
## 2.7.0 (2020-02-26)
* Feature: Add backpressure support and support throttling for streaming outgoing chunked request body.
(#148 by @clue)
* Feature: Start sending outgoing request even when streaming body doesn't emit any data yet.
(#150 by @clue)
* Feature: Only start request timeout timer after streaming request body has been sent (exclude upload time).
(#151 and #152 by @clue)
* Feature: Reject request when streaming request body emits error or closes unexpectedly.
(#153 by @clue)
* Improve download benchmarking script and add new upload benchmark.
(#149 by @clue)
## 2.6.1 (2020-01-14)
* Improve test suite by testing against PHP 7.4 and simplify test setup and test matrix
and fix testing redirected request when following relative redirect.
(#145 and #147 by @clue)
* Add support / sponsorship info and fix documentation typo.
(#144 by @clue and #133 by @eislambey)
## 2.6.0 (2019-04-03)
* Feature / Fix: Add `Content-Length: 0` request header for empty `POST` request etc.
(#120 by @clue)
* Fix: Only try to follow redirects if `Location` response header is present.
(#130 by @clue)
* Documentation and example for SSH proxy (SSH tunnel) and update SOCKS proxy example.
(#116, #119 and #121 by @clue)
* Improve test suite and also run tests on PHP 7.3.
(#122 by @samnela)
## 2.5.0 (2018-10-24)
* Feature: Add HTTP timeout option.
(#114 by @Rakdar and @clue)
This now respects PHP's `default_socket_timeout` setting (default 60s) as a
timeout for sending the outgoing HTTP request and waiting for a successful
response and will otherwise cancel the pending request and reject its value
with an Exception. You can now use the [`timeout` option](#withoptions) to
pass a custom timeout value in seconds like this:
```php
$browser = $browser->withOptions(array(
'timeout' => 10.0
));
$browser->get($uri)->then(function (ResponseInterface $response) {
// response received within 10 seconds maximum
var_dump($response->getHeaders());
});
```
Similarly, you can use a negative timeout value to not apply a timeout at
all or use a `null` value to restore the default handling.
* Improve documentation for `withOptions()` and
add documentation and example for HTTP CONNECT proxy.
(#111 and #115 by @clue)
* Refactor `Browser` to reuse single `Transaction` instance internally
which now accepts sending individual requests and their options.
(#113 by @clue)
## 2.4.0 (2018-10-02)
* Feature / Fix: Support cancellation forwarding and cancelling redirected requests.
(#110 by @clue)
* Feature / Fix: Remove `Authorization` request header for redirected cross-origin requests
and add documentation for HTTP redirects.
(#108 by @clue)
* Improve API documentation and add documentation for HTTP authentication and `Authorization` header.
(#104 and #109 by @clue)
* Update project homepage.
(#100 by @clue)
## 2.3.0 (2018-02-09)
* Feature / Fix: Pass custom request headers when following redirects
(#91 by @seregazhuk and #96 by @clue)
* Support legacy PHP 5.3 through PHP 7.2 and HHVM
(#95 by @clue)
* Improve documentation
(#87 by @holtkamp and #93 by @seregazhuk)
* Improve test suite by adding forward compatibility with PHPUnit 5, PHPUnit 6
and PHPUnit 7 and explicitly test HTTP/1.1 protocol version.
(#86 by @carusogabriel and #94 and #97 by @clue)
## 2.2.0 (2017-10-24)
* Feature: Forward compatibility with freshly released react/promise-stream v1.0
(#85 by @WyriHaximus)
## 2.1.0 (2017-09-17)
* Feature: Update minimum required Socket dependency version in order to
support Unix Domain Sockets (UDS) again,
support hosts file on all platforms and
work around sending secure HTTPS requests with PHP < 7.1.4
(#84 by @clue)
## 2.0.0 (2017-09-16)
A major compatibility release to update this component to support all latest
ReactPHP components!
This update involves a minor BC break due to dropped support for legacy
versions. 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: Remove deprecated API and mark Sender as @internal only,
remove all references to legacy SocketClient component and
remove support for Unix domain sockets (UDS) for now
(#77, #78, #81 and #83 by @clue)
> All of this affects the `Sender` only, which was previously marked as
"advanced usage" and is now marked `@internal` only. If you've not
used this class before, then this BC break will not affect you.
If you've previously used this class, then it's recommended to first
update to the intermediary v1.4.0 release, which allows you to use a
standard `ConnectorInterface` instead of the `Sender` and then update
to this version without causing a BC break.
If you've previously used Unix domain sockets (UDS), then you're
recommended to wait for the next version.
* Feature / BC break: Forward compatibility with future Stream v1.0 and strict stream semantics
(#79 by @clue)
> This component now follows strict stream semantics. This is marked as a
BC break because this removes undocumented and untested excessive event
arguments. If you've relied on proper stream semantics as documented
before, then this BC break will not affect you.
* Feature: Forward compatibility with future Socket and EventLoop components
(#80 by @clue)
## 1.4.0 (2017-09-15)
* Feature: `Browser` accepts `ConnectorInterface` and deprecate legacy `Sender`
(#76 by @clue)
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($loop, 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 Browser($loop, $connector);
```
## 1.3.0 (2017-09-08)
* Feature: Support request cancellation
(#75 by @clue)
```php
$promise = $browser->get($url);
$loop->addTimer(2.0, function () use ($promise) {
$promise->cancel();
});
```
* Feature: Update react/http-client to v0.5,
support react/stream v0.6 and react/socket-client v0.7 and drop legacy PHP 5.3 support
(#74 by @clue)
## 1.2.0 (2017-09-05)
* Feature: Forward compatibility with react/http-client v0.5
(#72 and #73 by @clue)
Older HttpClient versions are still supported, but the new version is now
preferred. Advanced usage with custom connectors now recommends setting up
the `React\HttpClient\Client` instance explicitly.
Accordingly, the `Sender::createFromLoopDns()` and
`Sender::createFromLoopConnectors()` have been marked as deprecated and
will be removed in future versions.
## 1.1.1 (2017-09-05)
* Restructure examples to ease getting started and
fix online tests and add option to exclude tests against httpbin.org
(#67 and #71 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
(#68 and #70 by @clue)
## 1.1.0 (2016-10-21)
* Feature: Obey explicitly set HTTP protocol version for outgoing requests
(#58, #59 by @WyriHaximus, @clue)
```php
$request = new Request('GET', $url);
$request = $request->withProtocolVersion(1.1);
$browser->send($request)->then(…);
```
## 1.0.1 (2016-08-12)
* Fix: Explicitly define all minimum required package versions
(#57 by @clue)
## 1.0.0 (2016-08-09)
* First stable release, now following SemVer
* Improve documentation and usage examples
> Contains no other changes, so it's actually fully compatible with the v0.5.0 release.
## 0.5.0 (2016-04-02)
* Feature / BC break: Implement PSR-7 http-message interfaces
(#54 by @clue)
Replace custom `Message`, `Request`, `Response` and `Uri` classes with
common PSR-7 interfaces:
```php
// old
$browser->get($uri)->then(function (Response $response) {
echo 'Test: ' . $response->getHeader('X-Test');
echo 'Body: ' . $response->getBody();
});
// new
$browser->get($uri)->then(function (ResponseInterface $response) {
if ($response->hasHeader('X-Test')) {
echo 'Test: ' . $response->getHeaderLine('X-Test');
}
echo 'Body: ' . $response->getBody();
});
```
* Feature: Add streaming API
(#56 by @clue)
```php
$browser = $browser->withOptions(array('streaming' => true));
$browser->get($uri)->then(function (ResponseInterface $response) {
$response->getBody()->on('data', function($chunk) {
echo $chunk . PHP_EOL;
});
});
```
* Remove / BC break: Remove `Browser::resolve()` because it's now fully decoupled
(#55 by @clue)
If you need this feature, consider explicitly depending on rize/uri-template
instead:
```bash
$ composer require rize/uri-template
```
* Use clue/block-react and new Promise API in order to simplify tests
(#53 by @clue)
## 0.4.2 (2016-03-25)
* Support advanced connection options with newest SocketClient (TLS/HTTPS and socket options)
(#51 by @clue)
* First class support for PHP 5.3 through PHP 7 and HHVM
(#52 by @clue)
## 0.4.1 (2015-09-05)
* Fix: Replace URI placeholders before applying base URI, in order to avoid
duplicate slashes introduced due to URI placeholders.
([#48](https://github.com/clue/php-buzz-react/pull/48))
```php
// now correctly returns "http://example.com/path"
// instead of previous "http://example.com//path"
$browser = $browser->withBase('http://example.com/');
echo $browser->resolve('{+path}', array('path' => '/path'));
// now correctly returns "http://example.com/path?q=test"
// instead of previous "http://example.com/path/?q=test"
$browser = $browser->withBase('http://example.com/path');
echo $browser->resolve('{?q}', array('q' => 'test'));
```
## 0.4.0 (2015-08-09)
* Feature: Resolve relative URIs, add withBase() and resolve()
([#41](https://github.com/clue/php-buzz-react/pull/41), [#44](https://github.com/clue/php-buzz-react/pull/44))
```php
$browser = $browser->withBase('http://example.com/');
$browser->post('/');
```
* Feature: Resolve URI template placeholders according to RFC 6570
([#42](https://github.com/clue/php-buzz-react/pull/42), [#44](https://github.com/clue/php-buzz-react/pull/44))
```php
$browser->post($browser->resolve('/{+path}{?version}', array(
'path' => 'demo.json',
'version' => '4'
)));
```
* Feature: Resolve and follow redirects to relative URIs
([#45](https://github.com/clue/php-buzz-react/pull/45))
* Feature / BC break: Simplify Request and Response objects.
Remove Browser::request(), use Browser::send() instead.
([#37](https://github.com/clue/php-buzz-react/pull/37))
```php
// old
$browser->request('GET', 'http://www.example.com/');
// new
$browser->send(new Request('GET', 'http://www.example.com/'));
```
* Feature / Bc break: Enforce absolute URIs via new Uri class
([#40](https://github.com/clue/php-buzz-react/pull/40), [#44](https://github.com/clue/php-buzz-react/pull/44))
* Feature: Add Browser::withSender() method
([#38](https://github.com/clue/php-buzz-react/pull/38))
* Feature: Add Sender::createFromLoopDns() function
([#39](https://github.com/clue/php-buzz-react/pull/39))
* Improve documentation and test suite
## 0.3.0 (2015-06-14)
* Feature: Expose Response object in case of HTTP errors
([#35](https://github.com/clue/php-buzz-react/pull/35))
* Feature: Add experimental `Transaction` options via `Browser`
([#25](https://github.com/clue/php-buzz-react/pull/25))
* Feature: Add experimental streaming API
([#31](https://github.com/clue/php-buzz-react/pull/31))
* Feature: Automatically assign a "Content-Length" header for outgoing `Request`s
([#29](https://github.com/clue/php-buzz-react/pull/29))
* Feature: Add `Message::getHeader()`, it is now available on both `Request` and `Response`
([#28](https://github.com/clue/php-buzz-react/pull/28))
## 0.2.0 (2014-11-30)
* Feature: Support communication via UNIX domain sockets
([#20](https://github.com/clue/php-buzz-react/pull/20))
* Fix: Detect immediately failing connection attempt
([#19](https://github.com/clue/php-buzz-react/issues/19))
## 0.1.2 (2014-10-28)
* Fix: Strict warning when accessing a single header value
([#18](https://github.com/clue/php-buzz-react/pull/18) by @masakielastic)
## 0.1.1 (2014-05-31)
* Compatibility with React PHP v0.4 (compatibility with v0.3 preserved)
([#11](https://github.com/clue/reactphp-buzz/pull/11))
## 0.1.0 (2014-05-27)
* First tagged release
## 0.0.0 (2013-09-01)
* Initial concept

21
vendor/clue/buzz-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Christian Lück
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.

1430
vendor/clue/buzz-react/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

38
vendor/clue/buzz-react/composer.json vendored Normal file
View File

@ -0,0 +1,38 @@
{
"name": "clue/buzz-react",
"description": "Simple, async PSR-7 HTTP client for concurrently processing any number of HTTP requests, built on top of ReactPHP",
"keywords": ["HTTP client", "PSR-7", "HTTP", "ReactPHP", "async"],
"homepage": "https://github.com/clue/reactphp-buzz",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"autoload": {
"psr-4": { "Clue\\React\\Buzz\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Clue\\Tests\\React\\Buzz\\": "tests/" }
},
"require": {
"php": ">=5.3",
"psr/http-message": "^1.0",
"react/event-loop": "^1.0 || ^0.5",
"react/http-client": "^0.5.10",
"react/promise": "^2.2.1 || ^1.2.1",
"react/promise-stream": "^1.0 || ^0.1.2",
"react/socket": "^1.1",
"react/stream": "^1.0 || ^0.7",
"ringcentral/psr7": "^1.2"
},
"require-dev": {
"clue/block-react": "^1.0",
"clue/http-proxy-react": "^1.3",
"clue/reactphp-ssh-proxy": "^1.0",
"clue/socks-react": "^1.0",
"phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35",
"react/http": "^0.8"
}
}

867
vendor/clue/buzz-react/src/Browser.php vendored Normal file
View File

@ -0,0 +1,867 @@
<?php
namespace Clue\React\Buzz;
use Clue\React\Buzz\Io\Sender;
use Clue\React\Buzz\Io\Transaction;
use Clue\React\Buzz\Message\MessageFactory;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
use React\EventLoop\LoopInterface;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Stream\ReadableStreamInterface;
class Browser
{
private $transaction;
private $messageFactory;
private $baseUrl;
private $protocolVersion = '1.1';
/**
* The `Browser` is responsible for sending HTTP requests to your HTTP server
* and keeps track of pending incoming HTTP responses.
* It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage).
*
* ```php
* $loop = React\EventLoop\Factory::create();
*
* $browser = new Clue\React\Buzz\Browser($loop);
* ```
*
* 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($loop, 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 Clue\React\Buzz\Browser($loop, $connector);
* ```
*
* @param LoopInterface $loop
* @param ConnectorInterface|null $connector [optional] Connector to use.
* Should be `null` in order to use default Connector.
*/
public function __construct(LoopInterface $loop, ConnectorInterface $connector = null)
{
$this->messageFactory = new MessageFactory();
$this->transaction = new Transaction(
Sender::createFromLoop($loop, $connector, $this->messageFactory),
$this->messageFactory,
$loop
);
}
/**
* Sends an HTTP GET request
*
* ```php
* $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
* var_dump((string)$response->getBody());
* });
* ```
*
* See also [example 01](../examples/01-google.php).
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $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()));
* });
* ```
*
* See also [example 04](../examples/04-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);
* ```
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $url URL for the request.
* @param array $headers
* @param string|ReadableStreamInterface $contents
* @return PromiseInterface<ResponseInterface>
*/
public function post($url, array $headers = array(), $contents = '')
{
return $this->requestMayBeStreaming('POST', $url, $headers, $contents);
}
/**
* Sends an HTTP HEAD request
*
* ```php
* $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
* var_dump($response->getHeaders());
* });
* ```
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $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()));
* });
* ```
*
* 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);
* ```
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $url URL for the request.
* @param array $headers
* @param string|ReadableStreamInterface $contents
* @return PromiseInterface<ResponseInterface>
*/
public function patch($url, array $headers = array(), $contents = '')
{
return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents);
}
/**
* 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());
* });
* ```
*
* See also [example 05](../examples/05-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);
* ```
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $url URL for the request.
* @param array $headers
* @param string|ReadableStreamInterface $contents
* @return PromiseInterface<ResponseInterface>
*/
public function put($url, array $headers = array(), $contents = '')
{
return $this->requestMayBeStreaming('PUT', $url, $headers, $contents);
}
/**
* Sends an HTTP DELETE request
*
* ```php
* $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) {
* var_dump((string)$response->getBody());
* });
* ```
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $url URL for the request.
* @param array $headers
* @param string|ReadableStreamInterface $contents
* @return PromiseInterface<ResponseInterface>
*/
public function delete($url, array $headers = array(), $contents = '')
{
return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents);
}
/**
* 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());
* });
* ```
*
* 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);
* ```
*
* > Note that this method is available as of v2.9.0 and always buffers the
* response body before resolving.
* It does not respect the deprecated [`streaming` option](#withoptions).
* If you want to stream the response body, you can use the
* [`requestStreaming()`](#requeststreaming) method instead.
*
* @param string $method HTTP request method, e.g. GET/HEAD/POST etc.
* @param string $url URL for the request
* @param array $headers Additional request headers
* @param string|ReadableStreamInterface $body HTTP request body contents
* @return PromiseInterface<ResponseInterface,Exception>
* @since 2.9.0
*/
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 $error) {
* echo 'Error: ' . $error->getMessage() . PHP_EOL;
* });
*
* $body->on('close', function () {
* echo '[DONE]' . 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);
* ```
*
* > Note that this method is available as of v2.9.0 and always resolves the
* response without buffering the response body.
* It does not respect the deprecated [`streaming` option](#withoptions).
* If you want to buffer the response body, use can use the
* [`request()`](#request) method instead.
*
* @param string $method HTTP request method, e.g. GET/HEAD/POST etc.
* @param string $url URL for the request
* @param array $headers Additional request headers
* @param string|ReadableStreamInterface $body HTTP request body contents
* @return PromiseInterface<ResponseInterface,Exception>
* @since 2.9.0
*/
public function requestStreaming($method, $url, $headers = array(), $contents = '')
{
return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents);
}
/**
* [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`).
*
* ```php
* // deprecated: see post() instead
* $browser->submit($url, array('user' => 'test', 'password' => 'secret'));
* ```
*
* This method will automatically add a matching `Content-Length` request
* header for the encoded length of the given `$fields`.
*
* > For BC reasons, this method accepts the `$url` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* @param string|UriInterface $url URL for the request.
* @param array $fields
* @param array $headers
* @param string $method
* @return PromiseInterface<ResponseInterface>
* @deprecated 2.9.0 See self::post() instead.
* @see self::post()
*/
public function submit($url, array $fields, $headers = array(), $method = 'POST')
{
$headers['Content-Type'] = 'application/x-www-form-urlencoded';
$contents = http_build_query($fields);
return $this->requestMayBeStreaming($method, $url, $headers, $contents);
}
/**
* [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7).
*
* 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
* $request = new Request('OPTIONS', $url);
*
* // deprecated: see request() instead
* $browser->send($request)->then();
* ```
*
* 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`).
*
* @param RequestInterface $request
* @return PromiseInterface<ResponseInterface>
* @deprecated 2.9.0 See self::request() instead.
* @see self::request()
*/
public function send(RequestInterface $request)
{
if ($this->baseUrl !== null) {
// ensure we're actually below the base URL
$request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl));
}
return $this->transaction->send($request);
}
/**
* 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
* $new = $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());
* });
* ```
*
* 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'));
* });
* ```
*
* 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());
* });
* ```
*
* 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 Clue\React\Buzz\Message\ResponseException) {
* // any HTTP response error message will now end up here
* $response = $e->getResponse();
* var_dump($response->getStatusCode(), $response->getReasonPhrase());
* } else {
* var_dump($e->getMessage());
* }
* });
* ```
*
* 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 prepending this absolute base URL. Note that this
* merely prepends the base URL and does *not* resolve any relative path
* references (like `../` etc.). This is mostly 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/example
* $browser->get('/example')->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.
*
* > For BC reasons, this method accepts the `$baseUrl` as either a `string`
* value or as an `UriInterface`. It's recommended to explicitly cast any
* objects implementing `UriInterface` to `string`.
*
* > Changelog: As of v2.9.0 this method accepts a `null` value to reset the
* base URL. Earlier versions had to use the deprecated `withoutBase()`
* method to reset the base URL.
*
* @param string|null|UriInterface $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 = $this->messageFactory->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
* $newBrowser = $browser->withProtocolVersion('1.0');
*
* $newBrowser->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
* @since 2.8.0
*/
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());
* });
* ```
*
* 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
));
}
/**
* [Deprecated] 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
* @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead.
* @see self::withTimeout()
* @see self::withFollowRedirects()
* @see self::withRejectErrorResponse()
*/
public function withOptions(array $options)
{
$browser = clone $this;
$browser->transaction = $this->transaction->withOptions($options);
return $browser;
}
/**
* [Deprecated] Removes the base URL.
*
* ```php
* // deprecated: see withBase() instead
* $newBrowser = $browser->withoutBase();
* ```
*
* Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method
* actually returns a *new* [`Browser`](#browser) instance without any base URL applied.
*
* See also [`withBase()`](#withbase).
*
* @return self
* @deprecated 2.9.0 See self::withBase() instead.
* @see self::withBase()
*/
public function withoutBase()
{
return $this->withBase(null);
}
/**
* @param string $method
* @param string|UriInterface $url
* @param array $headers
* @param string|ReadableStreamInterface $contents
* @return PromiseInterface<ResponseInterface,Exception>
*/
private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '')
{
return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion));
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Clue\React\Buzz\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
* @link https://github.com/reactphp/http/blob/master/src/Io/ChunkedEncoder.php Originally from react/http
*/
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();
}
}
}

161
vendor/clue/buzz-react/src/Io/Sender.php vendored Normal file
View File

@ -0,0 +1,161 @@
<?php
namespace Clue\React\Buzz\Io;
use Clue\React\Buzz\Message\MessageFactory;
use Psr\Http\Message\RequestInterface;
use React\EventLoop\LoopInterface;
use React\HttpClient\Client as HttpClient;
use React\HttpClient\Response as ResponseStream;
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($loop);
* $sender = \Clue\React\Buzz\Io\Sender::createFromLoop($loop, $connector);
* ```
*
* @param LoopInterface $loop
* @param ConnectorInterface|null $connector
* @return self
*/
public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null, MessageFactory $messageFactory)
{
return new self(new HttpClient($loop, $connector), $messageFactory);
}
private $http;
private $messageFactory;
/**
* [internal] Instantiate Sender
*
* @param HttpClient $http
* @internal
*/
public function __construct(HttpClient $http, MessageFactory $messageFactory)
{
$this->http = $http;
$this->messageFactory = $messageFactory;
}
/**
*
* @internal
* @param RequestInterface $request
* @return PromiseInterface Promise<ResponseInterface, Exception>
*/
public function send(RequestInterface $request)
{
$body = $request->getBody();
$size = $body->getSize();
if ($size !== null && $size !== 0) {
// automatically assign a "Content-Length" request header if the body size is known and non-empty
$request = $request->withHeader('Content-Length', (string)$size);
} elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) {
// only assign a "Content-Length: 0" request header if the body is expected for certain methods
$request = $request->withHeader('Content-Length', '0');
} elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) {
// use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown
$request = $request->withHeader('Transfer-Encoding', 'chunked');
} else {
// do not use chunked encoding if size is known or if this is an empty request body
$size = 0;
}
$headers = array();
foreach ($request->getHeaders() as $name => $values) {
$headers[$name] = implode(', ', $values);
}
$requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion());
$deferred = new Deferred(function ($_, $reject) use ($requestStream) {
// close request stream if request is cancelled
$reject(new \RuntimeException('Request cancelled'));
$requestStream->close();
});
$requestStream->on('error', function($error) use ($deferred) {
$deferred->reject($error);
});
$messageFactory = $this->messageFactory;
$requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) {
// apply response header values from response stream
$deferred->resolve($messageFactory->response(
$responseStream->getVersion(),
$responseStream->getCode(),
$responseStream->getReasonPhrase(),
$responseStream->getHeaders(),
$responseStream,
$request->getMethod()
));
});
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,305 @@
<?php
namespace Clue\React\Buzz\Io;
use Clue\React\Buzz\Message\ResponseException;
use Clue\React\Buzz\Message\MessageFactory;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\UriInterface;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
use React\Stream\ReadableStreamInterface;
/**
* @internal
*/
class Transaction
{
private $sender;
private $messageFactory;
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, MessageFactory $messageFactory, LoopInterface $loop)
{
$this->sender = $sender;
$this->messageFactory = $messageFactory;
$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->messageFactory, $this->loop);
$value = $default->$name;
}
$transaction->$name = $value;
}
}
return $transaction;
}
public function send(RequestInterface $request)
{
$deferred = new Deferred(function () use (&$deferred) {
if (isset($deferred->pending)) {
$deferred->pending->cancel();
unset($deferred->pending);
}
});
$deferred->numRequests = 0;
// use timeout from options or default to PHP's default_socket_timeout (60)
$timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout"));
$loop = $this->loop;
$this->next($request, $deferred)->then(
function (ResponseInterface $response) use ($deferred, $loop, &$timeout) {
if (isset($deferred->timeout)) {
$loop->cancelTimer($deferred->timeout);
unset($deferred->timeout);
}
$timeout = -1;
$deferred->resolve($response);
},
function ($e) use ($deferred, $loop, &$timeout) {
if (isset($deferred->timeout)) {
$loop->cancelTimer($deferred->timeout);
unset($deferred->timeout);
}
$timeout = -1;
$deferred->reject($e);
}
);
if ($timeout < 0) {
return $deferred->promise();
}
$body = $request->getBody();
if ($body instanceof ReadableStreamInterface && $body->isReadable()) {
$that = $this;
$body->on('close', function () use ($that, $deferred, &$timeout) {
if ($timeout >= 0) {
$that->applyTimeout($deferred, $timeout);
}
});
} else {
$this->applyTimeout($deferred, $timeout);
}
return $deferred->promise();
}
/**
* @internal
* @param Deferred $deferred
* @param number $timeout
* @return void
*/
public function applyTimeout(Deferred $deferred, $timeout)
{
$deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) {
$deferred->reject(new \RuntimeException(
'Request timed out after ' . $timeout . ' seconds'
));
if (isset($deferred->pending)) {
$deferred->pending->cancel();
unset($deferred->pending);
}
});
}
private function next(RequestInterface $request, Deferred $deferred)
{
$this->progress('request', array($request));
$that = $this;
++$deferred->numRequests;
$promise = $this->sender->send($request);
if (!$this->streaming) {
$promise = $promise->then(function ($response) use ($deferred, $that) {
return $that->bufferResponse($response, $deferred);
});
}
$deferred->pending = $promise;
return $promise->then(
function (ResponseInterface $response) use ($request, $that, $deferred) {
return $that->onResponse($response, $request, $deferred);
}
);
}
/**
* @internal
* @param ResponseInterface $response
* @return PromiseInterface Promise<ResponseInterface, Exception>
*/
public function bufferResponse(ResponseInterface $response, $deferred)
{
$stream = $response->getBody();
$size = $stream->getSize();
if ($size !== null && $size > $this->maximumSize) {
$stream->close();
return \React\Promise\reject(new \OverflowException(
'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0
));
}
// body is not streaming => already buffered
if (!$stream instanceof ReadableStreamInterface) {
return \React\Promise\resolve($response);
}
// buffer stream and resolve with buffered body
$messageFactory = $this->messageFactory;
$maximumSize = $this->maximumSize;
$promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then(
function ($body) use ($response, $messageFactory) {
return $response->withBody($messageFactory->body($body));
},
function ($e) use ($stream, $maximumSize) {
// try to close stream if buffering fails (or is cancelled)
$stream->close();
if ($e instanceof \OverflowException) {
$e = new \OverflowException(
'Response body size exceeds maximum of ' . $maximumSize . ' bytes',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0
);
}
throw $e;
}
);
$deferred->pending = $promise;
return $promise;
}
/**
* @internal
* @param ResponseInterface $response
* @param RequestInterface $request
* @throws ResponseException
* @return ResponseInterface|PromiseInterface
*/
public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred)
{
$this->progress('response', array($response, $request));
// follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled
// @link https://tools.ietf.org/html/rfc7231#section-6.4
if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) {
return $this->onResponseRedirect($response, $request, $deferred);
}
// only status codes 200-399 are considered to be valid, reject otherwise
if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) {
throw new ResponseException($response);
}
// resolve our initial promise
return $response;
}
/**
* @param ResponseInterface $response
* @param RequestInterface $request
* @return PromiseInterface
* @throws \RuntimeException
*/
private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred)
{
// resolve location relative to last request URI
$location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location'));
$request = $this->makeRedirectRequest($request, $location);
$this->progress('redirect', array($request));
if ($deferred->numRequests >= $this->maxRedirects) {
throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded');
}
return $this->next($request, $deferred);
}
/**
* @param RequestInterface $request
* @param UriInterface $location
* @return RequestInterface
*/
private function makeRedirectRequest(RequestInterface $request, UriInterface $location)
{
$originalHost = $request->getUri()->getHost();
$request = $request
->withoutHeader('Host')
->withoutHeader('Content-Type')
->withoutHeader('Content-Length');
// Remove authorization if changing hostnames (but not if just changing ports or protocols).
if ($location->getHost() !== $originalHost) {
$request = $request->withoutHeader('Authorization');
}
// naïve approach..
$method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET';
return $this->messageFactory->request($method, $location, $request->getHeaders());
}
private function progress($name, array $args = array())
{
return;
echo $name;
foreach ($args as $arg) {
echo ' ';
if ($arg instanceof ResponseInterface) {
echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase();
} elseif ($arg instanceof RequestInterface) {
echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion();
} else {
echo $arg;
}
}
echo PHP_EOL;
}
}

View File

@ -0,0 +1,139 @@
<?php
namespace Clue\React\Buzz\Message;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
use RingCentral\Psr7\Request;
use RingCentral\Psr7\Response;
use RingCentral\Psr7\Uri;
use React\Stream\ReadableStreamInterface;
/**
* @internal
*/
class MessageFactory
{
/**
* Creates a new instance of RequestInterface for the given request parameters
*
* @param string $method
* @param string|UriInterface $uri
* @param array $headers
* @param string|ReadableStreamInterface $content
* @param string $protocolVersion
* @return Request
*/
public function request($method, $uri, $headers = array(), $content = '', $protocolVersion = '1.1')
{
return new Request($method, $uri, $headers, $this->body($content), $protocolVersion);
}
/**
* Creates a new instance of ResponseInterface for the given response parameters
*
* @param string $protocolVersion
* @param int $status
* @param string $reason
* @param array $headers
* @param ReadableStreamInterface|string $body
* @param ?string $requestMethod
* @return Response
* @uses self::body()
*/
public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null)
{
$response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason);
if ($body instanceof ReadableStreamInterface) {
$length = null;
$code = $response->getStatusCode();
if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) {
$length = 0;
} elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') {
$length = null;
} elseif ($response->hasHeader('Content-Length')) {
$length = (int)$response->getHeaderLine('Content-Length');
}
$response = $response->withBody(new ReadableBodyStream($body, $length));
}
return $response;
}
/**
* Creates a new instance of StreamInterface for the given body contents
*
* @param ReadableStreamInterface|string $body
* @return StreamInterface
*/
public function body($body)
{
if ($body instanceof ReadableStreamInterface) {
return new ReadableBodyStream($body);
}
return \RingCentral\Psr7\stream_for($body);
}
/**
* Creates a new instance of UriInterface for the given URI string
*
* @param string $uri
* @return UriInterface
*/
public function uri($uri)
{
return new Uri($uri);
}
/**
* Creates a new instance of UriInterface for the given URI string relative to the given base URI
*
* @param UriInterface $base
* @param string $uri
* @return UriInterface
*/
public function uriRelative(UriInterface $base, $uri)
{
return Uri::resolve($base, $uri);
}
/**
* Resolves the given relative or absolute $uri by appending it behind $this base URI
*
* The given $uri parameter can be either a relative or absolute URI and
* as such can not contain any URI template placeholders.
*
* As such, the outcome of this method represents a valid, absolute URI
* which will be returned as an instance implementing `UriInterface`.
*
* If the given $uri is a relative URI, it will simply be appended behind $base URI.
*
* If the given $uri is an absolute URI, it will simply be returned as-is.
*
* @param UriInterface $uri
* @param UriInterface $base
* @return UriInterface
*/
public function expandBase(UriInterface $uri, UriInterface $base)
{
if ($uri->getScheme() !== '') {
return $uri;
}
$uri = (string)$uri;
$base = (string)$base;
if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') {
$base .= '/';
}
if (isset($uri[0]) && $uri[0] === '/') {
$uri = substr($uri, 1);
}
return $this->uri($base . $uri);
}
}

View File

@ -0,0 +1,153 @@
<?php
namespace Clue\React\Buzz\Message;
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,43 @@
<?php
namespace Clue\React\Buzz\Message;
use RuntimeException;
use Psr\Http\Message\ResponseInterface;
/**
* The `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.
*/
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 [`ResponseInterface`](#responseinterface) object.
*
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response;
}
}

View File

@ -0,0 +1,191 @@
# Changelog
## 1.3.0 (2022-08-30)
* Feature: Simplify usage by supporting new default loop.
(#33 by @SimonFrings)
```php
// old (still supported)
$connector = new ConnectionManagerTimeout($connector, 3.0, $loop);
$delayed = new ConnectionManagerDelayed($connector, 0.5, $loop);
// new (using default loop)
$connector = new ConnectionManagerTimeout($connector, 3.0);
$delayed = new ConnectionManagerDelayed($connector, 0.5);
```
* Feature: Full support for PHP 8.1 and PHP 8.2.
(#36 and #37 by @SimonFrings)
* Feature: Forward compatibility with upcoming Promise v3.
(#34 by @clue)
* Improve test suite and add badge to show number of project installations.
(#35 by @SimonFrings and #31 by @PaulRotmann)
## 1.2.0 (2020-12-12)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix.
(#24, #25 and #26 by @clue and #27, #28, #29 and #30 by @SimonFrings)
## 1.1.0 (2017-08-03)
* Feature: Support custom rejection reason for ConnectionManagerReject
(#23 by @clue)
```php
$connector = new ConnectionManagerReject(function ($uri) {
throw new RuntimeException($uri . ' blocked');
});
```
## 1.0.1 (2017-06-23)
* Fix: Ignore URI scheme when matching selective connectors
(#21 by @clue)
* Fix HHVM build for now again and ignore future HHVM build errors
(#22 by @clue)
## 1.0.0 (2017-05-09)
* First stable release, now following SemVer
> Contains no other changes, so it's actually fully compatible with the v0.7 releases.
## 0.7.1 (2017-05-09)
* Fix: Reject promise for invalid URI passed to ConnectionManagerSelective
(#19 by @clue)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 and
upcoming EventLoop v1.0 and v0.5
(#18 and #20 by @clue)
## 0.7.0 (2017-04-10)
* Feature / BC break: Replace deprecated SocketClient with new Socket component
(#17 by @clue)
This implies that all connectors from this package now implement the
`React\Socket\ConnectorInterface` instead of the legacy
`React\SocketClient\ConnectorInterface`.
## 0.6.0 (2017-04-07)
* Feature / BC break: Update SocketClient to v0.7 or v0.6
(#16 by @clue)
* Improve test suite by adding PHPUnit to require-dev
(#15 by @clue)
## 0.5.0 (2016-06-01)
* BC break: Change $retries to $tries
(#14 by @clue)
```php
// old
// 1 try plus 2 retries => 3 total tries
$c = new ConnectionManagerRepeat($c, 2);
// new
// 3 total tries (1 try plus 2 retries)
$c = new ConnectionManagerRepeat($c, 3);
```
* BC break: Timed connectors now use $loop as last argument
(#13 by @clue)
```php
// old
// $c = new ConnectionManagerDelay($c, $loop, 1.0);
$c = new ConnectionManagerTimeout($c, $loop, 1.0);
// new
$c = new ConnectionManagerTimeout($c, 1.0, $loop);
```
* BC break: Move all connector lists to the constructor
(#12 by @clue)
```php
// old
// $c = new ConnectionManagerConcurrent();
// $c = new ConnectionManagerRandom();
$c = new ConnectionManagerConsecutive();
$c->addConnectionManager($c1);
$c->addConnectionManager($c2);
// new
$c = new ConnectionManagerConsecutive(array(
$c1,
$c2
));
```
* BC break: ConnectionManagerSelective now accepts connector list in constructor
(#11 by @clue)
```php
// old
$c = new ConnectionManagerSelective();
$c->addConnectionManagerFor($c1, 'host1');
$c->addConnectionManagerFor($c2, 'host2');
// new
$c = new ConnectionManagerSelective(array(
'host1' => $c1,
'host2' => $c2
));
```
## 0.4.0 (2016-05-30)
* Feature: Add `ConnectionManagerConcurrent`
(#10 by @clue)
* Feature: Support Promise cancellation for all connectors
(#9 by @clue)
## 0.3.3 (2016-05-29)
* Fix repetitions for `ConnectionManagerRepeat`
(#8 by @clue)
* First class support for PHP 5.3 through PHP 7 and HHVM
(#7 by @clue)
## 0.3.2 (2016-03-19)
* Compatibility with react/socket-client:v0.5 (keeping full BC)
(#6 by @clue)
## 0.3.1 (2014-09-27)
* Support React PHP v0.4 (while preserving BC with React PHP v0.3)
(#4)
## 0.3.0 (2013-06-24)
* BC break: Switch from (deprecated) `clue/connection-manager` to `react/socket-client`
and thus replace each occurance of `getConnect($host, $port)` with `create($host, $port)`
(#1)
* Fix: Timeouts in `ConnectionManagerTimeout` now actually work
(#1)
* Fix: Properly reject promise in `ConnectionManagerSelective` when no targets
have been found
(#1)
## 0.2.0 (2013-02-08)
* Feature: Add `ConnectionManagerSelective` which works like a network/firewall ACL
## 0.1.0 (2013-01-12)
* First tagged release

View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Christian Lück
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.

View File

@ -0,0 +1,287 @@
# clue/reactphp-connection-manager-extra
[![CI status](https://github.com/clue/reactphp-connection-manager-extra/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-connection-manager-extra/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/connection-manager-extra?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/connection-manager-extra)
This project provides _extra_ (in terms of "additional", "extraordinary", "special" and "unusual") decorators,
built on top of [ReactPHP's Socket](https://github.com/reactphp/socket).
**Table of Contents**
* [Support us](#support-us)
* [Introduction](#introduction)
* [Usage](#usage)
* [Repeat](#repeat)
* [Timeout](#timeout)
* [Delay](#delay)
* [Reject](#reject)
* [Swappable](#swappable)
* [Consecutive](#consecutive)
* [Random](#random)
* [Concurrent](#concurrent)
* [Selective](#selective)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Introduction
If you're not already familar with [react/socket](https://github.com/reactphp/socket),
think of it as an async (non-blocking) version of [`fsockopen()`](https://www.php.net/manual/en/function.fsockopen.php)
or [`stream_socket_client()`](https://www.php.net/manual/en/function.stream-socket-client.php).
I.e. before you can send and receive data to/from a remote server, you first have to establish a connection - which
takes its time because it involves several steps.
In order to be able to establish several connections at the same time, [react/socket](https://github.com/reactphp/socket) provides a simple
API to establish simple connections in an async (non-blocking) way.
This project includes several classes that extend this base functionality by implementing the same simple `ConnectorInterface`.
This interface provides a single promise-based method `connect($uri)` which can be used to easily notify
when the connection is successfully established or the `Connector` gives up and the connection fails.
```php
$connector->connect('www.google.com:80')->then(function ($stream) {
echo 'connection successfully established';
$stream->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n");
$stream->end();
}, function ($exception) {
echo 'connection attempt failed: ' . $exception->getMessage();
});
```
Because everything uses the same simple API, the resulting `Connector` classes can be easily interchanged
and be used in places that expect the normal `ConnectorInterface`. This can be used to stack them into each other,
like using [timeouts](#timeout) for TCP connections, [delaying](#delay) SSL/TLS connections,
[retrying](#repeat) failed connection attempts, [randomly](#random) picking a `Connector` or
any combination thereof.
## Usage
This section lists all features of this library along with some examples.
The examples assume you've [installed](#install) this library and
already [set up a `Socket/Connector` instance `$connector`](https://github.com/reactphp/socket#connector).
All classes are located in the `ConnectionManager\Extra` namespace.
### Repeat
The `ConnectionManagerRepeat($connector, $tries)` tries connecting to the given location up to a maximum
of `$tries` times when the connection fails.
If you pass a value of `3` to it, it will first issue a normal connection attempt
and then retry up to 2 times if the connection attempt fails:
```php
$connectorRepeater = new ConnectionManagerRepeat($connector, 3);
$connectorRepeater->connect('www.google.com:80')->then(function ($stream) {
echo 'connection successfully established';
$stream->close();
});
```
### Timeout
The `ConnectionManagerTimeout($connector, $timeout, $loop = null)` sets a maximum `$timeout` in seconds on when to give up
waiting for the connection to complete.
```php
$connector = new ConnectionManagerTimeout($connector, 3.0);
```
### Delay
The `ConnectionManagerDelay($connector, $delay, $loop = null)` sets a fixed initial `$delay` in seconds before actually
trying to connect. (Not to be confused with [`ConnectionManagerTimeout`](#timeout) which sets a _maximum timeout_.)
```php
$delayed = new ConnectionManagerDelayed($connector, 0.5);
```
### Reject
The `ConnectionManagerReject(null|string|callable $reason)` simply rejects every single connection attempt.
This is particularly useful for the below [`ConnectionManagerSelective`](#selective) to reject connection attempts
to only certain destinations (for example blocking advertisements or harmful sites).
The constructor accepts an optional rejection reason which will be used for
rejecting the resulting promise.
You can explicitly pass a `string` value which will be used as the message for
the `Exception` instance:
```php
$connector = new ConnectionManagerReject('Blocked');
$connector->connect('www.google.com:80')->then(null, function ($e) {
assert($e instanceof \Exception);
assert($e->getMessage() === 'Blocked');
});
```
You can explicitly pass a `callable` value which will be used to either
`throw` or `return` a custom `Exception` instance:
```php
$connector = new ConnectionManagerReject(function ($uri) {
throw new RuntimeException($uri . ' blocked');
});
$connector->connect('www.google.com:80')->then(null, function ($e) {
assert($e instanceof \RuntimeException);
assert($e->getMessage() === 'www.google.com:80 blocked');
});
```
### Swappable
The `ConnectionManagerSwappable($connector)` is a simple decorator for other `ConnectionManager`s to
simplify exchanging the actual `ConnectionManager` during runtime (`->setConnectionManager($connector)`).
### Consecutive
The `ConnectionManagerConsecutive($connectors)` establishes connections by trying to connect through
any of the given `ConnectionManager`s in consecutive order until the first one succeeds.
```php
$consecutive = new ConnectionManagerConsecutive(array(
$connector1,
$connector2
));
```
### Random
The `ConnectionManagerRandom($connectors)` works much like `ConnectionManagerConsecutive` but instead
of using a fixed order, it always uses a randomly shuffled order.
```php
$random = new ConnectionManagerRandom(array(
$connector1,
$connector2
));
```
### Concurrent
The `ConnectionManagerConcurrent($connectors)` establishes connections by trying to connect through
ALL of the given `ConnectionManager`s at once, until the first one succeeds.
```php
$concurrent = new ConnectionManagerConcurrent(array(
$connector1,
$connector2
));
```
### Selective
The `ConnectionManagerSelective($connectors)` manages a list of `Connector`s and
forwards each connection through the first matching one.
This can be used to implement networking access control lists (ACLs) or firewall
rules like a blacklist or whitelist.
This allows fine-grained control on how to handle outgoing connections, like
rejecting advertisements, delaying unencrypted HTTP requests or forwarding HTTPS
connection through a foreign country.
If none of the entries in the list matches, the connection will be rejected.
This can be used to implement a very simple whitelist like this:
```php
$selective = new ConnectionManagerSelective(array(
'github.com' => $connector,
'*:443' => $connector
));
```
If you want to implement a blacklist (i.e. reject only certain targets), make
sure to add a default target to the end of the list like this:
```php
$reject = new ConnectionManagerReject();
$selective = new ConnectionManagerSelective(array(
'ads.example.com' => $reject,
'*:80-81' => $reject,
'*' => $connector
));
```
Similarly, you can also combine any of the other connectors to implement more
advanced connection setups, such as delaying unencrypted connections only and
retrying unreliable hosts:
```php
// delay connection by 2 seconds
$delayed = new ConnectionManagerDelay($connector, 2.0);
// maximum of 3 tries, each taking no longer than 2.0 seconds
$retry = new ConnectionManagerRepeat(
new ConnectionManagerTimeout($connector, 2.0),
3
);
$selective = new ConnectionManagerSelective(array(
'*:80' => $delayed,
'unreliable.example.com' => $retry,
'*' => $connector
));
```
Each entry in the list MUST be in the form `host` or `host:port`, where
`host` may contain the `*` wildcard character and `port` may be given as
either an exact port number or as a range in the form of `min-max`.
Passing anything else will result in an `InvalidArgumentException`.
> Note that the host will be matched exactly as-is otherwise. This means that
if you only block `youtube.com`, this has no effect on `www.youtube.com`.
You may want to add a second rule for `*.youtube.com` in this case.
## 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 clue/connection-manager-extra:^1.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 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
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

View File

@ -0,0 +1,29 @@
{
"name": "clue/connection-manager-extra",
"description": "Extra decorators for creating async TCP/IP connections, built on top of ReactPHP's Socket component",
"keywords": ["Socket", "network", "connection", "timeout", "delay", "reject", "repeat", "retry", "random", "acl", "firewall", "ReactPHP"],
"homepage": "https://github.com/clue/reactphp-connection-manager-extra",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"autoload": {
"psr-4": { "ConnectionManager\\Extra\\": "src" }
},
"autoload-dev": {
"psr-4": { "ConnectionManager\\Tests\\Extra\\": "tests/" }
},
"require": {
"php": ">=5.3",
"react/event-loop": "^1.2",
"react/promise": "^3 || ^2.1 || ^1.2.1",
"react/promise-timer": "^1.9",
"react/socket": "^1.12"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8"
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace ConnectionManager\Extra;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Timer;
use React\Socket\ConnectorInterface;
class ConnectionManagerDelay implements ConnectorInterface
{
/** @var ConnectorInterface */
private $connectionManager;
/** @var float */
private $delay;
/** @var LoopInterface */
private $loop;
/**
* @param ConnectorInterface $connectionManager
* @param float $delay
* @param ?LoopInterface $loop
*/
public function __construct(ConnectorInterface $connectionManager, $delay, LoopInterface $loop = null)
{
$this->connectionManager = $connectionManager;
$this->delay = $delay;
$this->loop = $loop ?: Loop::get();
}
public function connect($uri)
{
$connectionManager = $this->connectionManager;
return Timer\resolve($this->delay, $this->loop)->then(function () use ($connectionManager, $uri) {
return $connectionManager->connect($uri);
});
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace ConnectionManager\Extra;
use React\Socket\ConnectorInterface;
use React\Promise;
use Exception;
// a simple connection manager that rejects every single connection attempt
class ConnectionManagerReject implements ConnectorInterface
{
private $reason = 'Connection rejected';
/**
* @param null|string|callable $reason
*/
public function __construct($reason = null)
{
if ($reason !== null) {
$this->reason = $reason;
}
}
public function connect($uri)
{
$reason = $this->reason;
if (!is_string($reason)) {
try {
$reason = $reason($uri);
} catch (\Exception $e) {
$reason = $e;
}
}
if (!$reason instanceof \Exception) {
$reason = new Exception($reason);
}
return Promise\reject($reason);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace ConnectionManager\Extra;
use React\Socket\ConnectorInterface;
use InvalidArgumentException;
use Exception;
use React\Promise\Promise;
use React\Promise\PromiseInterface;
class ConnectionManagerRepeat implements ConnectorInterface
{
protected $connectionManager;
protected $maximumTries;
public function __construct(ConnectorInterface $connectionManager, $maximumTries)
{
if ($maximumTries < 1) {
throw new InvalidArgumentException('Maximum number of tries must be >= 1');
}
$this->connectionManager = $connectionManager;
$this->maximumTries = $maximumTries;
}
public function connect($uri)
{
$tries = $this->maximumTries;
$connector = $this->connectionManager;
return new Promise(function ($resolve, $reject) use ($uri, &$pending, &$tries, $connector) {
$try = function ($error = null) use (&$try, &$pending, &$tries, $uri, $connector, $resolve, $reject) {
if ($tries > 0) {
--$tries;
$pending = $connector->connect($uri);
$pending->then($resolve, $try);
} else {
$reject(new Exception('Connection still fails even after retrying', 0, $error));
}
};
$try();
}, function ($_, $reject) use (&$pending, &$tries) {
// stop retrying, reject results and cancel pending attempt
$tries = 0;
$reject(new \RuntimeException('Cancelled'));
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
});
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace ConnectionManager\Extra;
use React\Socket\ConnectorInterface;
// connection manager decorator which simplifies exchanging the actual connection manager during runtime
class ConnectionManagerSwappable implements ConnectorInterface
{
protected $connectionManager;
public function __construct(ConnectorInterface $connectionManager)
{
$this->connectionManager = $connectionManager;
}
public function connect($uri)
{
return $this->connectionManager->connect($uri);
}
public function setConnectionManager(ConnectorInterface $connectionManager)
{
$this->connectionManager = $connectionManager;
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace ConnectionManager\Extra;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Timer;
use React\Socket\ConnectorInterface;
class ConnectionManagerTimeout implements ConnectorInterface
{
/** @var ConnectorInterface */
private $connectionManager;
/** @var float */
private $timeout;
/** @var LoopInterface */
private $loop;
/**
* @param ConnectorInterface $connectionManager
* @param float $timeout
* @param ?LoopInterface $loop
*/
public function __construct(ConnectorInterface $connectionManager, $timeout, LoopInterface $loop = null)
{
$this->connectionManager = $connectionManager;
$this->timeout = $timeout;
$this->loop = $loop ?: Loop::get();
}
public function connect($uri)
{
$promise = $this->connectionManager->connect($uri);
return Timer\timeout($promise, $this->timeout, $this->loop)->then(null, function ($e) use ($promise) {
// connection successfully established but timeout already expired => close successful connection
$promise->then(function ($connection) {
$connection->end();
});
throw $e;
});
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace ConnectionManager\Extra\Multiple;
use React\Promise;
use React\Promise\PromiseInterface;
class ConnectionManagerConcurrent extends ConnectionManagerConsecutive
{
public function connect($uri)
{
$all = array();
foreach ($this->managers as $connector) {
$all []= $connector->connect($uri);
}
return Promise\any($all)->then(function ($conn) use ($all) {
// a connection attempt succeeded
// => cancel all pending connection attempts
foreach ($all as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
// if promise resolves despite cancellation, immediately close stream
$promise->then(function ($stream) use ($conn) {
if ($stream !== $conn) {
$stream->close();
}
});
}
return $conn;
});
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace ConnectionManager\Extra\Multiple;
use React\Promise;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use UnderflowException;
class ConnectionManagerConsecutive implements ConnectorInterface
{
protected $managers;
/**
*
* @param ConnectorInterface[] $managers
*/
public function __construct(array $managers)
{
if (!$managers) {
throw new \InvalidArgumentException('List of connectors must not be empty');
}
$this->managers = $managers;
}
public function connect($uri)
{
return $this->tryConnection($this->managers, $uri);
}
/**
*
* @param ConnectorInterface[] $managers
* @param string $uri
* @return Promise
* @internal
*/
public function tryConnection(array $managers, $uri)
{
return new Promise\Promise(function ($resolve, $reject) use (&$managers, &$pending, $uri) {
$try = function () use (&$try, &$managers, $uri, $resolve, $reject, &$pending) {
if (!$managers) {
return $reject(new UnderflowException('No more managers to try to connect through'));
}
$manager = array_shift($managers);
$pending = $manager->connect($uri);
$pending->then($resolve, $try);
};
$try();
}, function ($_, $reject) use (&$managers, &$pending) {
// stop retrying, reject results and cancel pending attempt
$managers = array();
$reject(new \RuntimeException('Cancelled'));
if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) {
$pending->cancel();
}
});
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace ConnectionManager\Extra\Multiple;
class ConnectionManagerRandom extends ConnectionManagerConsecutive
{
public function connect($uri)
{
$managers = $this->managers;
shuffle($managers);
return $this->tryConnection($managers, $uri);
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace ConnectionManager\Extra\Multiple;
use React\Socket\ConnectorInterface;
use React\Promise;
use UnderflowException;
use InvalidArgumentException;
class ConnectionManagerSelective implements ConnectorInterface
{
private $managers;
/**
*
* @param ConnectorInterface[] $managers
*/
public function __construct(array $managers)
{
foreach ($managers as $filter => $manager) {
$host = $filter;
$portMin = 0;
$portMax = 65535;
// search colon (either single one OR preceded by "]" due to IPv6)
$colon = strrpos($host, ':');
if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) {
if (!isset($host[$colon + 1])) {
throw new InvalidArgumentException('Entry "' . $filter . '" has no port after colon');
}
$minus = strpos($host, '-', $colon);
if ($minus === false) {
$portMin = $portMax = (int)substr($host, $colon + 1);
if (substr($host, $colon + 1) !== (string)$portMin) {
throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port after colon');
}
} else {
$portMin = (int)substr($host, $colon + 1, ($minus - $colon));
$portMax = (int)substr($host, $minus + 1);
if (substr($host, $colon + 1) !== ($portMin . '-' . $portMax)) {
throw new InvalidArgumentException('Entry "' . $filter . '" has no valid port range after colon');
}
if ($portMin > $portMax) {
throw new InvalidArgumentException('Entry "' . $filter . '" has port range mixed up');
}
}
$host = substr($host, 0, $colon);
}
if ($host === '') {
throw new InvalidArgumentException('Entry "' . $filter . '" has an empty host');
}
if (!$manager instanceof ConnectorInterface) {
throw new InvalidArgumentException('Entry "' . $filter . '" is not a valid connector');
}
}
$this->managers = $managers;
}
public function connect($uri)
{
$parts = parse_url((strpos($uri, '://') === false ? 'tcp://' : '') . $uri);
if (!isset($parts) || !isset($parts['scheme'], $parts['host'], $parts['port'])) {
return Promise\reject(new InvalidArgumentException('Invalid URI'));
}
$connector = $this->getConnectorForTarget(
trim($parts['host'], '[]'),
$parts['port']
);
if ($connector === null) {
return Promise\reject(new UnderflowException('No connector for given target found'));
}
return $connector->connect($uri);
}
private function getConnectorForTarget($targetHost, $targetPort)
{
foreach ($this->managers as $host => $connector) {
$portMin = 0;
$portMax = 65535;
// search colon (either single one OR preceded by "]" due to IPv6)
$colon = strrpos($host, ':');
if ($colon !== false && (strpos($host, ':') === $colon || substr($host, $colon - 1, 1) === ']' )) {
$minus = strpos($host, '-', $colon);
if ($minus === false) {
$portMin = $portMax = (int)substr($host, $colon + 1);
} else {
$portMin = (int)substr($host, $colon + 1, ($minus - $colon));
$portMax = (int)substr($host, $minus + 1);
}
$host = trim(substr($host, 0, $colon), '[]');
}
if ($targetPort >= $portMin && $targetPort <= $portMax && fnmatch($host, $targetHost)) {
return $connector;
}
}
return null;
}
}

View File

@ -0,0 +1,200 @@
# Changelog
## 1.8.0 (2022-09-01)
* Feature: Full support for PHP 8.1 and PHP 8.2.
(#47 and #48 by @SimonFrings)
* Feature: Mark passwords and URIs as `#[\SensitiveParameter]` (PHP 8.2+).
(#49 by @SimonFrings)
* Feature: Forward compatibility with upcoming Promise v3.
(#44 by @clue)
* Fix: Fix invalid references in exception stack trace.
(#45 by @clue)
* Improve test suite and fix legacy HHVM build.
(#46 by @SimonFrings)
## 1.7.0 (2021-08-06)
* Feature: Simplify usage by supporting new default loop and making `Connector` optional.
(#41 and #42 by @clue)
```php
// old (still supported)
$proxy = new Clue\React\HttpProxy\ProxyConnector(
'127.0.0.1:8080',
new React\Socket\Connector($loop)
);
// new (using default loop)
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
```
* Documentation improvements and updated examples.
(#39 and #43 by @clue and #40 by @PaulRotmann)
* Improve test suite and use GitHub actions for continuous integration (CI).
(#38 by @SimonFrings)
## 1.6.0 (2020-10-23)
* Enhanced documentation for ReactPHP's new HTTP client.
(#35 and #37 by @SimonFrings)
* Improve test suite, prepare PHP 8 support and support PHPUnit 9.3.
(#36 by @SimonFrings)
## 1.5.0 (2020-06-19)
* Feature / Fix: Support PHP 7.4 by skipping unneeded cleanup of exception trace args.
(#33 by @clue)
* Clean up test suite and add `.gitattributes` to exclude dev files from exports.
Run tests on PHP 7.4, PHPUnit 9 and simplify test matrix.
Link to using SSH proxy (SSH tunnel) as an alternative.
(#27 by @clue and #31, #32 and #34 by @SimonFrings)
## 1.4.0 (2018-10-30)
* Feature: Improve error reporting for failed connection attempts and improve
cancellation forwarding during proxy connection setup.
(#23 and #26 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.
Similarly, any underlying connection issues to the proxy server will now be
reported as part of the previous exception.
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 = $proxy->connect('tcp://example.com:80');
$promise->then(function (ConnectionInterface $connection) {
// …
}, function (Exception $e) {
echo $e->getMessage();
});
```
* Feature: Add support for custom HTTP request headers.
(#25 by @valga and @clue)
```php
// new: now supports custom HTTP request headers
$proxy = new ProxyConnector('127.0.0.1:8080', $connector, array(
'Proxy-Authorization' => 'Bearer abc123',
'User-Agent' => 'ReactPHP'
));
```
* Fix: Fix connecting to IPv6 destination hosts.
(#22 by @clue)
* Link to clue/reactphp-buzz for HTTP requests and update project homepage.
(#21 and #24 by @clue)
## 1.3.0 (2018-02-13)
* Feature: Support communication over Unix domain sockets (UDS)
(#20 by @clue)
```php
// new: now supports communication over Unix domain sockets (UDS)
$proxy = new ProxyConnector('http+unix:///tmp/proxy.sock', $connector);
```
* Reduce memory consumption by avoiding circular reference from stream reader
(#18 by @valga)
* Improve documentation
(#19 by @clue)
## 1.2.0 (2017-08-30)
* Feature: Use socket error codes for connection rejections
(#17 by @clue)
```php
$promise = $proxy->connect('imap.example.com:143');
$promise->then(null, function (Exeption $e) {
if ($e->getCode() === SOCKET_EACCES) {
echo 'Failed to authenticate with proxy!';
}
throw $e;
});
```
* Improve test suite by locking Travis distro so new defaults will not break the build and
optionally exclude tests that rely on working internet connection
(#15 and #16 by @clue)
## 1.1.0 (2017-06-11)
* Feature: Support proxy authentication if proxy URL contains username/password
(#14 by @clue)
```php
// new: username/password will now be passed to HTTP proxy server
$proxy = new ProxyConnector('user:pass@127.0.0.1:8080', $connector);
```
## 1.0.0 (2017-06-10)
* First stable release, now following SemVer
> Contains no other changes, so it's actually fully compatible with the v0.3.2 release.
## 0.3.2 (2017-06-10)
* Fix: Fix rejecting invalid URIs and unexpected URI schemes
(#13 by @clue)
* Fix HHVM build for now again and ignore future HHVM build errors
(#12 by @clue)
* Documentation for Connector concepts (TCP/TLS, timeouts, DNS resolution)
(#11 by @clue)
## 0.3.1 (2017-05-10)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#10 by @clue)
## 0.3.0 (2017-04-10)
* Feature / BC break: Replace deprecated SocketClient with new Socket component
(#9 by @clue)
This implies that the `ProxyConnector` from this package now implements the
`React\Socket\ConnectorInterface` instead of the legacy
`React\SocketClient\ConnectorInterface`.
## 0.2.0 (2017-04-10)
* Feature / BC break: Update SocketClient to v0.7 or v0.6 and
use `connect($uri)` instead of `create($host, $port)`
(#8 by @clue)
```php
// old
$connector->create($host, $port)->then(function (Stream $conn) {
$conn->write("…");
});
// new
$connector->connect($uri)->then(function (ConnectionInterface $conn) {
$conn->write("…");
});
```
* Improve test suite by adding PHPUnit to require-dev
(#7 by @clue)
## 0.1.0 (2016-11-01)
* First tagged release

21
vendor/clue/http-proxy-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2016 Christian Lück
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.

510
vendor/clue/http-proxy-react/README.md vendored Normal file
View File

@ -0,0 +1,510 @@
# clue/reactphp-http-proxy
[![CI status](https://github.com/clue/reactphp-http-proxy/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-http-proxy/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/http-proxy-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/http-proxy-react)
Async HTTP proxy connector, tunnel any TCP/IP-based protocol through an HTTP
CONNECT proxy server, built on top of [ReactPHP](https://reactphp.org/).
HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy")
are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to
conceal the origin address (anonymity) or to circumvent address blocking
(geoblocking). While many (public) HTTP CONNECT proxy servers often limit this
to HTTPS port `443` only, this can technically be used to tunnel any
TCP/IP-based protocol (HTTP, SMTP, IMAP etc.).
This library provides a simple API to create these tunneled connections for you.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
This makes it fairly simple to add HTTP CONNECT proxy support to pretty much any
existing higher-level protocol implementation.
* **Async execution of connections** -
Send any number of HTTP CONNECT requests in parallel and process their
responses as soon as results come in.
The Promise-based design provides a *sane* interface to working with out of
order responses and possible connection errors.
* **Standard interfaces** -
Allows easy integration with existing higher-level components by implementing
ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface).
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Builds on top of well-tested components and well-established concepts instead of reinventing the wheel.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested against actual proxy servers in the wild.
**Table of contents**
* [Support us](#support-us)
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [ProxyConnector](#proxyconnector)
* [Plain TCP connections](#plain-tcp-connections)
* [Secure TLS connections](#secure-tls-connections)
* [HTTP requests](#http-requests)
* [Connection timeout](#connection-timeout)
* [DNS resolution](#dns-resolution)
* [Authentication](#authentication)
* [Advanced HTTP headers](#advanced-http-headers)
* [Advanced secure proxy connections](#advanced-secure-proxy-connections)
* [Advanced Unix domain sockets](#advanced-unix-domain-sockets)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [More](#more)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Quickstart example
The following example code demonstrates how this library can be used to send a
secure HTTPS request to google.com through a local HTTP proxy server:
```php
<?php
require __DIR__ . '/vendor/autoload.php';
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => false
));
$browser = new React\Http\Browser($connector);
$browser->get('https://google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
var_dump($response->getHeaders(), (string) $response->getBody());
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
See also the [examples](examples).
## Usage
### ProxyConnector
The `ProxyConnector` is responsible for creating plain TCP/IP connections to
any destination by using an intermediary HTTP CONNECT proxy.
```
[you] -> [proxy] -> [destination]
```
Its constructor simply accepts an HTTP proxy URL with the proxy server address:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
```
The proxy URL may or may not contain a scheme and port definition. The default
port will be `80` for HTTP (or `443` for HTTPS), but many common HTTP proxy
servers use custom ports (often the alternative HTTP port `8080`).
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
)
));
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080', $connector);
```
This is the main class in this package.
Because it implements ReactPHP's standard
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface),
it can simply be used in place of a normal connector.
Accordingly, it provides only a single public method, the
[`connect()`](https://github.com/reactphp/socket#connect) method.
The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>`
method can be used to establish a streaming connection.
It returns a [Promise](https://github.com/reactphp/promise) which either
fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface)
on success or rejects with an `Exception` on error.
This makes it fairly simple to add HTTP CONNECT proxy support to pretty much any
higher-level component:
```diff
- $acme = new AcmeApi($connector);
+ $proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080', $connector);
+ $acme = new AcmeApi($proxy);
```
#### Plain TCP connections
HTTP CONNECT proxies are most frequently used to issue HTTPS requests to your destination.
However, this is actually performed on a higher protocol layer and this
connector is actually inherently a general-purpose plain TCP/IP connector.
As documented above, you can simply invoke its `connect()` method to establish
a streaming plain TCP/IP connection and use any higher level protocol like so:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$proxy->connect('tcp://smtp.googlemail.com:587')->then(function (React\Socket\ConnectionInterface $connection) {
$connection->write("EHLO local\r\n");
$connection->on('data', function ($chunk) use ($connection) {
echo $chunk;
});
});
```
You can either use the `ProxyConnector` directly or you may want to wrap this connector
in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector):
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => false
));
$connector->connect('tcp://smtp.googlemail.com:587')->then(function (React\Socket\ConnectionInterface $connection) {
$connection->write("EHLO local\r\n");
$connection->on('data', function ($chunk) use ($connection) {
echo $chunk;
});
});
```
Note that HTTP CONNECT proxies often restrict which ports one may connect to.
Many (public) proxy servers do in fact limit this to HTTPS (443) only.
#### Secure TLS connections
This class can also be used if you want to establish a secure TLS connection
(formerly known as SSL) between you and your destination, such as when using
secure HTTPS to your destination site. You can simply wrap this connector in
ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector):
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => false
));
$connector->connect('tls://smtp.googlemail.com:465')->then(function (React\Socket\ConnectionInterface $connection) {
$connection->write("EHLO local\r\n");
$connection->on('data', function ($chunk) use ($connection) {
echo $chunk;
});
});
```
> Note how secure TLS connections are in fact entirely handled outside of
this HTTP CONNECT client implementation.
#### HTTP requests
This library also allows you to send HTTP requests through an HTTP CONNECT proxy server.
In order to send HTTP requests, you first have to add a dependency for
[ReactPHP's async HTTP client](https://github.com/reactphp/http#client-usage).
This allows you to send both plain HTTP and TLS-encrypted HTTPS requests like this:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => false
));
$browser = new React\Http\Browser($connector);
$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
var_dump($response->getHeaders(), (string) $response->getBody());
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
See also [ReactPHP's HTTP client](https://github.com/reactphp/http#client-usage)
and any of the [examples](examples) for more details.
#### Connection timeout
By default, the `ProxyConnector` does not implement any timeouts for establishing remote
connections.
Your underlying operating system may impose limits on pending and/or idle TCP/IP
connections, anywhere in a range of a few minutes to several hours.
Many use cases require more control over the timeout and likely values much
smaller, usually in the range of a few seconds only.
You can use ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector)
to decorate any given `ConnectorInterface` instance.
It provides the same `connect()` method, but will automatically reject the
underlying connection attempt if it takes too long:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => false,
'timeout' => 3.0
));
$connector->connect('tcp://google.com:80')->then(function ($connection) {
// connection succeeded within 3.0 seconds
});
```
See also any of the [examples](examples).
> Note how the connection timeout is in fact entirely handled outside of this
HTTP CONNECT client implementation.
#### DNS resolution
By default, the `ProxyConnector` does not perform any DNS resolution at all and simply
forwards any hostname you're trying to connect to the remote proxy server.
The remote proxy server is thus responsible for looking up any hostnames via DNS
(this default mode is thus called *remote DNS resolution*).
As an alternative, you can also send the destination IP to the remote proxy
server.
In this mode you either have to stick to using IPs only (which is ofen unfeasable)
or perform any DNS lookups locally and only transmit the resolved destination IPs
(this mode is thus called *local DNS resolution*).
The default *remote DNS resolution* is useful if your local `ProxyConnector` either can
not resolve target hostnames because it has no direct access to the internet or
if it should not resolve target hostnames because its outgoing DNS traffic might
be intercepted.
As noted above, the `ProxyConnector` defaults to using remote DNS resolution.
However, wrapping the `ProxyConnector` in ReactPHP's
[`Connector`](https://github.com/reactphp/socket#connector) actually
performs local DNS resolution unless explicitly defined otherwise.
Given that remote DNS resolution is assumed to be the preferred mode, all
other examples explicitly disable DNS resolution like this:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => false
));
```
If you want to explicitly use *local DNS resolution*, you can use the following code:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080');
// set up Connector which uses Google's public DNS (8.8.8.8)
$connector = new React\Socket\Connector(array(
'tcp' => $proxy,
'dns' => '8.8.8.8'
));
```
> Note how local DNS resolution is in fact entirely handled outside of this
HTTP CONNECT client implementation.
#### Authentication
If your HTTP proxy server requires authentication, you may pass the username and
password as part of the HTTP proxy URL like this:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('alice:password@127.0.0.1:8080');
```
Note that both the username and password must be percent-encoded if they contain
special characters:
```php
$user = 'he:llo';
$pass = 'p@ss';
$url = rawurlencode($user) . ':' . rawurlencode($pass) . '@127.0.0.1:8080';
$proxy = new Clue\React\HttpProxy\ProxyConnector($url);
```
> The authentication details will be used for basic authentication and will be
transferred in the `Proxy-Authorization` HTTP request header for each
connection attempt.
If the authentication details are missing or not accepted by the remote HTTP
proxy server, it is expected to reject each connection attempt with a
`407` (Proxy Authentication Required) response status code and an exception
error code of `SOCKET_EACCES` (13).
#### Advanced HTTP headers
The `ProxyConnector` constructor accepts an optional array of custom request
headers to send in the `CONNECT` request. This can be useful if you're using a
custom proxy setup or authentication scheme if the proxy server does not support
basic [authentication](#authentication) as documented above. This is rarely used
in practice, but may be useful for some more advanced use cases. In this case,
you may simply pass an assoc array of additional request headers like this:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector(
'127.0.0.1:8080',
null,
array(
'Proxy-Authorization' => 'Bearer abc123',
'User-Agent' => 'ReactPHP'
)
);
```
#### Advanced secure proxy connections
Note that communication between the client and the proxy is usually via an
unencrypted, plain TCP/IP HTTP connection. Note that this is the most common
setup, because you can still establish a TLS connection between you and the
destination host as above.
If you want to connect to a (rather rare) HTTPS proxy, you may want use the
`https://` scheme (HTTPS default port 443) to create a secure connection to the proxy:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('https://127.0.0.1:443');
$proxy->connect('tcp://smtp.googlemail.com:587');
```
#### Advanced Unix domain sockets
HTTP CONNECT proxy servers support forwarding TCP/IP based connections and
higher level protocols.
In some advanced cases, it may be useful to let your HTTP CONNECT proxy server
listen on a Unix domain socket (UDS) path instead of a IP:port combination.
For example, this allows you to rely on file system permissions instead of
having to rely on explicit [authentication](#authentication).
You can simply use the `http+unix://` URI scheme like this:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('http+unix:///tmp/proxy.sock');
$proxy->connect('tcp://google.com:80')->then(function (React\Socket\ConnectionInterface $connection) {
// connected…
});
```
Similarly, you can also combine this with [authentication](#authentication)
like this:
```php
$proxy = new Clue\React\HttpProxy\ProxyConnector('http+unix://alice:password@/tmp/proxy.sock');
```
> Note that Unix domain sockets (UDS) are considered advanced usage and PHP only
has limited support for this.
In particular, enabling [secure TLS](#secure-tls-connections) may not be
supported.
> Note that the HTTP CONNECT protocol does not support the notion of UDS paths.
The above works reasonably well because UDS is only used for the connection between
client and proxy server and the path will not actually passed over the protocol.
This implies that this does not support connecting to UDS destination paths.
## 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 clue/http-proxy-react:^1.8
```
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 contains tests that rely on a working internet connection,
alternatively you can also run it like this:
```bash
vendor/bin/phpunit --exclude-group internet
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.
## More
* If you want to learn more about how the
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
and its usual implementations look like, refer to the documentation of the underlying
[react/socket](https://github.com/reactphp/socket) component.
* If you want to learn more about processing streams of data, refer to the
documentation of the underlying
[react/stream](https://github.com/reactphp/stream) component.
* As an alternative to an HTTP CONNECT proxy, you may also want to look into
using a SOCKS (SOCKS4/SOCKS5) proxy instead.
You may want to use [clue/reactphp-socks](https://github.com/clue/reactphp-socks)
which also provides an implementation of the same
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
so that supporting either proxy protocol should be fairly trivial.
* As an alternative to an HTTP CONNECT proxy, you may also want to look into
using an SSH proxy (SSH tunnel) instead.
You may want to use [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy)
which also provides an implementation of the same
[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface)
so that supporting either proxy protocol should be fairly trivial.
* If you're dealing with public proxies, you'll likely have to work with mixed
quality and unreliable proxies. You may want to look into using
[clue/reactphp-connection-manager-extra](https://github.com/clue/reactphp-connection-manager-extra)
which allows retrying unreliable ones, implying connection timeouts,
concurrently working with multiple connectors and more.
* If you're looking for an end-user HTTP CONNECT proxy server daemon, you may
want to use [LeProxy](https://leproxy.org/).

View File

@ -0,0 +1,31 @@
{
"name": "clue/http-proxy-react",
"description": "Async HTTP proxy connector, tunnel any TCP/IP-based protocol through an HTTP CONNECT proxy server, built on top of ReactPHP",
"keywords": ["HTTP", "CONNECT", "proxy", "ReactPHP", "async"],
"homepage": "https://github.com/clue/reactphp-http-proxy",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"require": {
"php": ">=5.3",
"react/promise": "^3 || ^2.1 || ^1.2.1",
"react/socket": "^1.12",
"ringcentral/psr7": "^1.2"
},
"require-dev": {
"clue/block-react": "^1.5",
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8",
"react/event-loop": "^1.2",
"react/http": "^1.5"
},
"autoload": {
"psr-4": { "Clue\\React\\HttpProxy\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Clue\\Tests\\React\\HttpProxy\\": "tests/" }
}
}

View File

@ -0,0 +1,278 @@
<?php
namespace Clue\React\HttpProxy;
use Exception;
use InvalidArgumentException;
use RuntimeException;
use RingCentral\Psr7;
use React\Promise;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
use React\Socket\UnixConnector;
/**
* A simple Connector that uses an HTTP CONNECT proxy to create plain TCP/IP connections to any destination
*
* [you] -> [proxy] -> [destination]
*
* This is most frequently used to issue HTTPS requests to your destination.
* However, this is actually performed on a higher protocol layer and this
* connector is actually inherently a general-purpose plain TCP/IP connector.
*
* Note that HTTP CONNECT proxies often restrict which ports one may connect to.
* Many (public) proxy servers do in fact limit this to HTTPS (443) only.
*
* If you want to establish a TLS connection (such as HTTPS) between you and
* your destination, you may want to wrap this connector in a SecureConnector
* instance.
*
* Note that communication between the client and the proxy is usually via an
* unencrypted, plain TCP/IP HTTP connection. Note that this is the most common
* setup, because you can still establish a TLS connection between you and the
* destination host as above.
*
* If you want to connect to a (rather rare) HTTPS proxy, you may want use its
* HTTPS port (443) and use a SecureConnector instance to create a secure
* connection to the proxy.
*
* @link https://tools.ietf.org/html/rfc7231#section-4.3.6
*/
class ProxyConnector implements ConnectorInterface
{
private $connector;
private $proxyUri;
private $headers = '';
/**
* Instantiate a new ProxyConnector which uses the given $proxyUrl
*
* @param string $proxyUrl The proxy URL may or may not contain a scheme and
* port definition. The default port will be `80` for HTTP (or `443` for
* HTTPS), but many common HTTP proxy servers use custom ports.
* @param ?ConnectorInterface $connector (Optional) Connector to use.
* @param array $httpHeaders Custom HTTP headers to be sent to the proxy.
* @throws InvalidArgumentException if the proxy URL is invalid
*/
public function __construct(
#[\SensitiveParameter]
$proxyUrl,
ConnectorInterface $connector = null,
array $httpHeaders = array()
) {
// support `http+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^http\+unix:\/\/(.*?@)?(.+?)$/', $proxyUrl, $match)) {
// rewrite URI to parse authentication from dummy host
$proxyUrl = 'http://' . $match[1] . 'localhost';
// connector uses Unix transport scheme and explicit path given
$connector = new FixedUriConnector(
'unix://' . $match[2],
$connector ?: new UnixConnector()
);
}
if (strpos($proxyUrl, '://') === false) {
$proxyUrl = 'http://' . $proxyUrl;
}
$parts = parse_url($proxyUrl);
if (!$parts || !isset($parts['scheme'], $parts['host']) || ($parts['scheme'] !== 'http' && $parts['scheme'] !== 'https')) {
throw new InvalidArgumentException('Invalid proxy URL "' . $proxyUrl . '"');
}
// apply default port and TCP/TLS transport for given scheme
if (!isset($parts['port'])) {
$parts['port'] = $parts['scheme'] === 'https' ? 443 : 80;
}
$parts['scheme'] = $parts['scheme'] === 'https' ? 'tls' : 'tcp';
$this->connector = $connector ?: new Connector();
$this->proxyUri = $parts['scheme'] . '://' . $parts['host'] . ':' . $parts['port'];
// prepare Proxy-Authorization header if URI contains username/password
if (isset($parts['user']) || isset($parts['pass'])) {
$this->headers = 'Proxy-Authorization: Basic ' . base64_encode(
rawurldecode($parts['user'] . ':' . (isset($parts['pass']) ? $parts['pass'] : ''))
) . "\r\n";
}
// append any additional custom request headers
foreach ($httpHeaders as $name => $values) {
foreach ((array)$values as $value) {
$this->headers .= $name . ': ' . $value . "\r\n";
}
}
}
public function connect($uri)
{
if (strpos($uri, '://') === false) {
$uri = 'tcp://' . $uri;
}
$parts = parse_url($uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
return Promise\reject(new InvalidArgumentException('Invalid target URI specified'));
}
$target = $parts['host'] . ':' . $parts['port'];
// construct URI to HTTP CONNECT proxy server to connect to
$proxyUri = $this->proxyUri;
// append path from URI if given
if (isset($parts['path'])) {
$proxyUri .= $parts['path'];
}
// parse query args
$args = array();
if (isset($parts['query'])) {
parse_str($parts['query'], $args);
}
// append hostname from URI to query string unless explicitly given
if (!isset($args['hostname'])) {
$args['hostname'] = trim($parts['host'], '[]');
}
// append query string
$proxyUri .= '?' . http_build_query($args, '', '&');
// append fragment from URI if given
if (isset($parts['fragment'])) {
$proxyUri .= '#' . $parts['fragment'];
}
$connecting = $this->connector->connect($proxyUri);
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
$reject(new RuntimeException(
'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
));
// either close active connection or cancel pending connection attempt
$connecting->then(function (ConnectionInterface $stream) {
$stream->close();
});
$connecting->cancel();
});
$headers = $this->headers;
$connecting->then(function (ConnectionInterface $stream) use ($target, $headers, $deferred, $uri) {
// keep buffering data until headers are complete
$buffer = '';
$stream->on('data', $fn = function ($chunk) use (&$buffer, $deferred, $stream, &$fn, $uri) {
$buffer .= $chunk;
$pos = strpos($buffer, "\r\n\r\n");
if ($pos !== false) {
// end of headers received => stop buffering
$stream->removeListener('data', $fn);
$fn = null;
// try to parse headers as response message
try {
$response = Psr7\parse_response(substr($buffer, 0, $pos));
} catch (Exception $e) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
$e
));
$stream->close();
return;
}
if ($response->getStatusCode() === 407) {
// map status code 407 (Proxy Authentication Required) to EACCES
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
));
$stream->close();
return;
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
// map non-2xx status code to ECONNREFUSED
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
));
$stream->close();
return;
}
// all okay, resolve with stream instance
$deferred->resolve($stream);
// emit remaining incoming as data event
$buffer = (string)substr($buffer, $pos + 4);
if ($buffer !== '') {
$stream->emit('data', array($buffer));
$buffer = '';
}
return;
}
// stop buffering when 8 KiB have been read
if (isset($buffer[8192])) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because proxy response headers exceed maximum of 8 KiB (EMSGSIZE)',
defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90
));
$stream->close();
}
});
$stream->on('error', function (Exception $e) use ($deferred, $uri) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
defined('SOCKET_EIO') ? SOCKET_EIO : 5,
$e
));
});
$stream->on('close', function () use ($deferred, $uri) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response (ECONNRESET)',
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104
));
});
$stream->write("CONNECT " . $target . " HTTP/1.1\r\nHost: " . $target . "\r\n" . $headers . "\r\n");
}, function (Exception $e) use ($deferred, $uri) {
$deferred->reject($e = new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
$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);
});
return $deferred->promise();
}
}

85
vendor/clue/mq-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,85 @@
# Changelog
## 1.5.0 (2022-09-30)
* Feature: Forward compatibility with upcoming Promise v3.
(#33 by @clue)
* Update to use new reactphp/async package instead of clue/reactphp-block.
(#34 by @SimonFrings)
## 1.4.0 (2021-11-15)
* Feature: Support PHP 8.1, avoid deprecation warning concerning `\Countable::count(...)` return type.
(#32 by @bartvanhoutte)
* Improve documentation and simplify examples by updating to new [default loop](https://reactphp.org/event-loop/#loop).
(#27 and #29 by @PaulRotmann and #30 by @SimonFrings)
* Improve test suite to use GitHub actions for continuous integration (CI).
(#28 by @SimonFrings)
## 1.3.0 (2020-10-16)
* Enhanced documentation for ReactPHP's new HTTP client and
add support / sponsorship info.
(#21 and #24 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix.
(#22, #23 and #25 by @SimonFrings)
## 1.2.0 (2019-12-05)
* Feature: Add `any()` helper to await first successful fulfillment of operations.
(#18 by @clue)
```php
// new: limit concurrency while awaiting any operation to complete
$promise = Queue::any(3, $urls, function ($url) use ($browser) {
return $browser->get($url);
});
$promise->then(function (ResponseInterface $response) {
echo 'First successful: ' . $response->getStatusCode() . PHP_EOL;
});
```
* Minor documentation improvements (fix syntax issues and typos) and update examples.
(#9 and #11 by @clue and #15 by @holtkamp)
* Improve test suite to test against PHP 7.4 and PHP 7.3, drop legacy HHVM support,
update distro on Travis and update project homepage.
(#10 and #19 by @clue)
## 1.1.0 (2018-04-30)
* Feature: Add `all()` helper to await successful fulfillment of all operations
(#8 by @clue)
```php
// new: limit concurrency while awaiting all operations to complete
$promise = Queue::all(3, $urls, function ($url) use ($browser) {
return $browser->get($url);
});
$promise->then(function (array $responses) {
echo 'All ' . count($responses) . ' successful!' . PHP_EOL;
});
```
* Fix: Implement cancellation forwarding for previously queued operations
(#7 by @clue)
## 1.0.0 (2018-02-26)
* First stable release, following SemVer
I'd like to thank [Bergfreunde GmbH](https://www.bergfreunde.de/), a German
online retailer for Outdoor Gear & Clothing, for sponsoring the first release! 🎉
Thanks to sponsors like this, who understand the importance of open source
development, I can justify spending time and focus on open source development
instead of traditional paid work.
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

21
vendor/clue/mq-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2018 Christian Lück
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.

523
vendor/clue/mq-react/README.md vendored Normal file
View File

@ -0,0 +1,523 @@
# clue/reactphp-mq
[![CI status](https://github.com/clue/reactphp-mq/workflows/CI/badge.svg)](https://github.com/clue/reactphp-mq/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/mq-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/mq-react)
Mini Queue, the lightweight in-memory message queue to concurrently do many (but not too many) things at once,
built on top of [ReactPHP](https://reactphp.org/).
Let's say you crawl a page and find that you need to send 100 HTTP requests to
following pages which each takes `0.2s`. You can either send them all
sequentially (taking around `20s`) or you can use
[ReactPHP](https://reactphp.org) to concurrently request all your pages at the
same time. This works perfectly fine for a small number of operations, but
sending an excessive number of requests can either take up all resources on your
side or may get you banned by the remote side as it sees an unreasonable number
of requests from your side.
Instead, you can use this library to effectively rate limit your operations and
queue excessives ones so that not too many operations are processed at once.
This library provides a simple API that is easy to use in order to manage any
kind of async operation without having to mess with most of the low-level details.
You can use this to throttle multiple HTTP requests, database queries or pretty
much any API that already uses Promises.
* **Async execution of operations** -
Process any number of async operations and choose how many should be handled
concurrently and how many operations can be queued in-memory. Process their
results as soon as responses come in.
The Promise-based design provides a *sane* interface to working with out of order results.
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Builds on top of well-tested components and well-established concepts instead of reinventing the wheel.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested in the *real world*.
**Table of contents**
* [Support us](#support-us)
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [Queue](#queue)
* [Promises](#promises)
* [Cancellation](#cancellation)
* [Timeout](#timeout)
* [all()](#all)
* [any()](#any)
* [Blocking](#blocking)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Quickstart example
Once [installed](#install), you can use the following code to access an
HTTP webserver and send a large number of HTTP GET requests:
```php
<?php
require __DIR__ . '/vendor/autoload.php';
$browser = new React\Http\Browser();
// load a huge array of URLs to fetch
$urls = file('urls.txt');
// each job should use the browser to GET a certain URL
// limit number of concurrent jobs here
$q = new Clue\React\Mq\Queue(3, null, function ($url) use ($browser) {
return $browser->get($url);
});
foreach ($urls as $url) {
$q($url)->then(function (Psr\Http\Message\ResponseInterface $response) use ($url) {
echo $url . ': ' . $response->getBody()->getSize() . ' bytes' . PHP_EOL;
});
}
```
See also the [examples](examples).
## Usage
### Queue
The `Queue` is responsible for managing your operations and ensuring not too
many operations are executed at once. It's a very simple and lightweight
in-memory implementation of the
[leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket#As_a_queue) algorithm.
This means that you control how many operations can be executed concurrently.
If you add a job to the queue and it still below the limit, it will be executed
immediately. If you keep adding new jobs to the queue and its concurrency limit
is reached, it will not start a new operation and instead queue this for future
execution. Once one of the pending operations complete, it will pick the next
job from the queue and execute this operation.
The `new Queue(int $concurrency, ?int $limit, callable $handler)` call
can be used to create a new queue instance.
You can create any number of queues, for example when you want to apply
different limits to different kinds of operations.
The `$concurrency` parameter sets a new soft limit for the maximum number
of jobs to handle concurrently. Finding a good concurrency limit depends
on your particular use case. It's common to limit concurrency to a rather
small value, as doing more than a dozen of things at once may easily
overwhelm the receiving side.
The `$limit` parameter sets a new hard limit on how many jobs may be
outstanding (kept in memory) at once. Depending on your particular use
case, it's usually safe to keep a few hundreds or thousands of jobs in
memory. If you do not want to apply an upper limit, you can pass a `null`
value which is semantically more meaningful than passing a big number.
```php
// handle up to 10 jobs concurrently, but keep no more than 1000 in memory
$q = new Queue(10, 1000, $handler);
```
```php
// handle up to 10 jobs concurrently, do not limit queue size
$q = new Queue(10, null, $handler);
```
```php
// handle up to 10 jobs concurrently, reject all further jobs
$q = new Queue(10, 10, $handler);
```
The `$handler` parameter must be a valid callable that accepts your job
parameters, invokes the appropriate operation and returns a Promise as a
placeholder for its future result.
```php
// using a Closure as handler is usually recommended
$q = new Queue(10, null, function ($url) use ($browser) {
return $browser->get($url);
});
```
```php
// accepts any callable, so PHP's array notation is also supported
$q = new Queue(10, null, array($browser, 'get'));
```
#### Promises
This library works under the assumption that you want to concurrently handle
async operations that use a [Promise](https://github.com/reactphp/promise)-based API.
The demonstration purposes, the examples in this documentation use
[ReactPHP's async HTTP client](https://github.com/reactphp/http#client-usage), but you
may use any Promise-based API with this project. Its API can be used like this:
```php
$browser = new React\Http\Browser();
$promise = $browser->get($url);
```
If you wrap this in a `Queue` instance as given above, this code will look
like this:
```php
$browser = new React\Http\Browser();
$q = new Queue(10, null, function ($url) use ($browser) {
return $browser->get($url);
});
$promise = $q($url);
```
The `$q` instance is invokable, so that invoking `$q(...$args)` will
actually be forwarded as `$browser->get(...$args)` as given in the
`$handler` argument when concurrency is still below limits.
Each operation is expected to be async (non-blocking), so you may actually
invoke multiple operations concurrently (send multiple requests in parallel).
The `$handler` is responsible for responding to each request with a resolution
value, the order is not guaranteed.
These operations use a [Promise](https://github.com/reactphp/promise)-based
interface that makes it easy to react to when an operation is completed (i.e.
either successfully fulfilled or rejected with an error):
```php
$promise->then(
function ($result) {
var_dump('Result received', $result);
},
function (Exception $error) {
var_dump('There was an error', $error->getMessage());
}
);
```
Each operation may take some time to complete, but due to its async nature you
can actually start any number of (queued) operations. Once the concurrency limit
is reached, this invocation will simply be queued and this will return a pending
promise which will start the actual operation once another operation is
completed. This means that this is handled entirely transparently and you do not
need to worry about this concurrency limit yourself.
If this looks strange to you, you can also use the more traditional
[blocking API](#blocking).
#### Cancellation
The returned Promise is implemented in such a way that it can be cancelled
when it is still pending.
Cancelling a pending operation will invoke its cancellation handler which is
responsible for rejecting its value with an Exception and cleaning up any
underlying resources.
```php
$promise = $q($url);
Loop::addTimer(2.0, function () use ($promise) {
$promise->cancel();
});
```
Similarly, cancelling an operation that is queued and has not yet been started
will be rejected without ever starting the operation.
#### Timeout
By default, this library does not limit how long a single operation can take,
so that the resulting promise may stay pending for a long time.
Many use cases involve some kind of "timeout" logic so that an operation is
cancelled after a certain threshold is reached.
You can simply use [cancellation](#cancellation) as in the previous chapter or
you may want to look into using [react/promise-timer](https://github.com/reactphp/promise-timer)
which helps taking care of this through a simple API.
The resulting code with timeouts applied look something like this:
```php
use React\Promise\Timer;
$q = new Queue(10, null, function ($uri) use ($browser) {
return Timer\timeout($browser->get($uri), 2.0);
});
$promise = $q($uri);
```
The resulting promise can be consumed as usual and the above code will ensure
that execution of this operation can not take longer than the given timeout
(i.e. after it is actually started).
In particular, note how this differs from applying a timeout to the resulting
promise. The following code will ensure that the total time for queuing and
executing this operation can not take longer than the given timeout:
```php
// usually not recommended
$promise = Timer\timeout($q($url), 2.0);
```
Please refer to [react/promise-timer](https://github.com/reactphp/promise-timer)
for more details.
#### all()
The static `all(int $concurrency, array $jobs, callable $handler): PromiseInterface<mixed[]>` method can be used to
concurrently process all given jobs through the given `$handler`.
This is a convenience method which uses the `Queue` internally to
schedule all jobs while limiting concurrency to ensure no more than
`$concurrency` jobs ever run at once. It will return a promise which
resolves with the results of all jobs on success.
```php
$browser = new React\Http\Browser();
$promise = Queue::all(3, $urls, function ($url) use ($browser) {
return $browser->get($url);
});
$promise->then(function (array $responses) {
echo 'All ' . count($responses) . ' successful!' . PHP_EOL;
});
```
If either of the jobs fail, it will reject the resulting promise and will
try to cancel all outstanding jobs. Similarly, calling `cancel()` on the
resulting promise will try to cancel all outstanding jobs. See
[promises](#promises) and [cancellation](#cancellation) for details.
The `$concurrency` parameter sets a new soft limit for the maximum number
of jobs to handle concurrently. Finding a good concurrency limit depends
on your particular use case. It's common to limit concurrency to a rather
small value, as doing more than a dozen of things at once may easily
overwhelm the receiving side. Using a `1` value will ensure that all jobs
are processed one after another, effectively creating a "waterfall" of
jobs. Using a value less than 1 will reject with an
`InvalidArgumentException` without processing any jobs.
```php
// handle up to 10 jobs concurrently
$promise = Queue::all(10, $jobs, $handler);
```
```php
// handle each job after another without concurrency (waterfall)
$promise = Queue::all(1, $jobs, $handler);
```
The `$jobs` parameter must be an array with all jobs to process. Each
value in this array will be passed to the `$handler` to start one job.
The array keys will be preserved in the resulting array, while the array
values will be replaced with the job results as returned by the
`$handler`. If this array is empty, this method will resolve with an
empty array without processing any jobs.
The `$handler` parameter must be a valid callable that accepts your job
parameters, invokes the appropriate operation and returns a Promise as a
placeholder for its future result. If the given argument is not a valid
callable, this method will reject with an `InvalidArgumentException`
without processing any jobs.
```php
// using a Closure as handler is usually recommended
$promise = Queue::all(10, $jobs, function ($url) use ($browser) {
return $browser->get($url);
});
```
```php
// accepts any callable, so PHP's array notation is also supported
$promise = Queue::all(10, $jobs, array($browser, 'get'));
```
> Keep in mind that returning an array of response messages means that
the whole response body has to be kept in memory.
#### any()
The static `any(int $concurrency, array $jobs, callable $handler): PromiseInterface<mixed>` method can be used to
concurrently process the given jobs through the given `$handler` and
resolve with first resolution value.
This is a convenience method which uses the `Queue` internally to
schedule all jobs while limiting concurrency to ensure no more than
`$concurrency` jobs ever run at once. It will return a promise which
resolves with the result of the first job on success and will then try
to `cancel()` all outstanding jobs.
```php
$browser = new React\Http\Browser();
$promise = Queue::any(3, $urls, function ($url) use ($browser) {
return $browser->get($url);
});
$promise->then(function (ResponseInterface $response) {
echo 'First response: ' . $response->getBody() . PHP_EOL;
});
```
If all of the jobs fail, it will reject the resulting promise. Similarly,
calling `cancel()` on the resulting promise will try to cancel all
outstanding jobs. See [promises](#promises) and
[cancellation](#cancellation) for details.
The `$concurrency` parameter sets a new soft limit for the maximum number
of jobs to handle concurrently. Finding a good concurrency limit depends
on your particular use case. It's common to limit concurrency to a rather
small value, as doing more than a dozen of things at once may easily
overwhelm the receiving side. Using a `1` value will ensure that all jobs
are processed one after another, effectively creating a "waterfall" of
jobs. Using a value less than 1 will reject with an
`InvalidArgumentException` without processing any jobs.
```php
// handle up to 10 jobs concurrently
$promise = Queue::any(10, $jobs, $handler);
```
```php
// handle each job after another without concurrency (waterfall)
$promise = Queue::any(1, $jobs, $handler);
```
The `$jobs` parameter must be an array with all jobs to process. Each
value in this array will be passed to the `$handler` to start one job.
The array keys have no effect, the promise will simply resolve with the
job results of the first successful job as returned by the `$handler`.
If this array is empty, this method will reject without processing any
jobs.
The `$handler` parameter must be a valid callable that accepts your job
parameters, invokes the appropriate operation and returns a Promise as a
placeholder for its future result. If the given argument is not a valid
callable, this method will reject with an `InvalidArgumentExceptionn`
without processing any jobs.
```php
// using a Closure as handler is usually recommended
$promise = Queue::any(10, $jobs, function ($url) use ($browser) {
return $browser->get($url);
});
```
```php
// accepts any callable, so PHP's array notation is also supported
$promise = Queue::any(10, $jobs, array($browser, 'get'));
```
#### Blocking
As stated above, this library provides you a powerful, async API by default.
You can also integrate this into your traditional, blocking environment by using
[reactphp/async](https://github.com/reactphp/async). This allows you to simply
await async HTTP requests like this:
```php
use function React\Async\await;
$browser = new React\Http\Browser();
$promise = Queue::all(3, $urls, function ($url) use ($browser) {
return $browser->get($url);
});
try {
$responses = await($promise);
// responses successfully received
} catch (Exception $e) {
// an error occured while performing the requests
}
```
Similarly, you can also wrap this in a function to provide a simple API and hide
all the async details from the outside:
```php
use function React\Async\await;
/**
* Concurrently downloads all the given URIs
*
* @param string[] $uris list of URIs to download
* @return ResponseInterface[] map with a response object for each URI
* @throws Exception if any of the URIs can not be downloaded
*/
function download(array $uris)
{
$browser = new React\Http\Browser();
$promise = Queue::all(3, $uris, function ($uri) use ($browser) {
return $browser->get($uri);
});
return await($promise);
}
```
This is made possible thanks to fibers available in PHP 8.1+ and our
compatibility API that also works on all supported PHP versions.
Please refer to [reactphp/async](https://github.com/reactphp/async#readme) for more details.
> Keep in mind that returning an array of response messages means that the whole
response body has to be kept in memory.
## 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 clue/mq-react:^1.5
```
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+.
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
```
## License
This project is released under the permissive [MIT license](LICENSE).
I'd like to thank [Bergfreunde GmbH](https://www.bergfreunde.de/), a German
online retailer for Outdoor Gear & Clothing, for sponsoring the first release! 🎉
Thanks to sponsors like this, who understand the importance of open source
development, I can justify spending time and focus on open source development
instead of traditional paid work.
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

29
vendor/clue/mq-react/composer.json vendored Normal file
View File

@ -0,0 +1,29 @@
{
"name": "clue/mq-react",
"description": "Mini Queue, the lightweight in-memory message queue to concurrently do many (but not too many) things at once, built on top of ReactPHP",
"keywords": ["Message Queue", "Mini Queue", "job", "message", "worker", "queue", "rate limit", "throttle", "concurrency", "ReactPHP", "async"],
"homepage": "https://github.com/clue/reactphp-mq",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"autoload": {
"psr-4": { "Clue\\React\\Mq\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Clue\\Tests\\React\\Mq\\": "tests/" }
},
"require": {
"php": ">=5.3",
"react/promise": "^3 || ^2.2.1 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
"react/async": "^4 || ^3 || ^2",
"react/event-loop": "^1.2",
"react/http": "^1.8"
}
}

447
vendor/clue/mq-react/src/Queue.php vendored Normal file
View File

@ -0,0 +1,447 @@
<?php
namespace Clue\React\Mq;
use React\Promise;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
/**
* The `Queue` is responsible for managing your operations and ensuring not too
* many operations are executed at once. It's a very simple and lightweight
* in-memory implementation of the
* [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket#As_a_queue) algorithm.
*
* This means that you control how many operations can be executed concurrently.
* If you add a job to the queue and it still below the limit, it will be executed
* immediately. If you keep adding new jobs to the queue and its concurrency limit
* is reached, it will not start a new operation and instead queue this for future
* execution. Once one of the pending operations complete, it will pick the next
* job from the queue and execute this operation.
*/
class Queue implements \Countable
{
private $concurrency;
private $limit;
private $handler;
private $pending = 0;
private $queue = array();
/**
* Concurrently process all given jobs through the given `$handler`.
*
* This is a convenience method which uses the `Queue` internally to
* schedule all jobs while limiting concurrency to ensure no more than
* `$concurrency` jobs ever run at once. It will return a promise which
* resolves with the results of all jobs on success.
*
* ```php
* $browser = new React\Http\Browser();
*
* $promise = Queue::all(3, $urls, function ($url) use ($browser) {
* return $browser->get($url);
* });
*
* $promise->then(function (array $responses) {
* echo 'All ' . count($responses) . ' successful!' . PHP_EOL;
* });
* ```
*
* If either of the jobs fail, it will reject the resulting promise and will
* try to cancel all outstanding jobs. Similarly, calling `cancel()` on the
* resulting promise will try to cancel all outstanding jobs. See
* [promises](#promises) and [cancellation](#cancellation) for details.
*
* The `$concurrency` parameter sets a new soft limit for the maximum number
* of jobs to handle concurrently. Finding a good concurrency limit depends
* on your particular use case. It's common to limit concurrency to a rather
* small value, as doing more than a dozen of things at once may easily
* overwhelm the receiving side. Using a `1` value will ensure that all jobs
* are processed one after another, effectively creating a "waterfall" of
* jobs. Using a value less than 1 will reject with an
* `InvalidArgumentException` without processing any jobs.
*
* ```php
* // handle up to 10 jobs concurrently
* $promise = Queue::all(10, $jobs, $handler);
* ```
*
* ```php
* // handle each job after another without concurrency (waterfall)
* $promise = Queue::all(1, $jobs, $handler);
* ```
*
* The `$jobs` parameter must be an array with all jobs to process. Each
* value in this array will be passed to the `$handler` to start one job.
* The array keys will be preserved in the resulting array, while the array
* values will be replaced with the job results as returned by the
* `$handler`. If this array is empty, this method will resolve with an
* empty array without processing any jobs.
*
* The `$handler` parameter must be a valid callable that accepts your job
* parameters, invokes the appropriate operation and returns a Promise as a
* placeholder for its future result. If the given argument is not a valid
* callable, this method will reject with an `InvalidArgumentException`
* without processing any jobs.
*
* ```php
* // using a Closure as handler is usually recommended
* $promise = Queue::all(10, $jobs, function ($url) use ($browser) {
* return $browser->get($url);
* });
* ```
*
* ```php
* // accepts any callable, so PHP's array notation is also supported
* $promise = Queue::all(10, $jobs, array($browser, 'get'));
* ```
*
* > Keep in mind that returning an array of response messages means that
* the whole response body has to be kept in memory.
*
* @param int $concurrency concurrency soft limit
* @param array $jobs
* @param callable $handler
* @return PromiseInterface Returns a Promise<mixed[]> which resolves with an array of all resolution values
* or rejects when any of the operations reject.
*/
public static function all($concurrency, array $jobs, $handler)
{
try {
// limit number of concurrent operations
$q = new self($concurrency, null, $handler);
} catch (\InvalidArgumentException $e) {
// reject if $concurrency or $handler is invalid
return Promise\reject($e);
}
// try invoking all operations and automatically queue excessive ones
$promises = array_map($q, $jobs);
return new Promise\Promise(function ($resolve, $reject) use ($promises) {
Promise\all($promises)->then($resolve, function ($e) use ($promises, $reject) {
// cancel all pending promises if a single promise fails
foreach (array_reverse($promises) as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
// reject with original rejection message
$reject($e);
});
}, function () use ($promises) {
// cancel all pending promises on cancellation
foreach (array_reverse($promises) as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
});
}
/**
* Concurrently process the given jobs through the given `$handler` and
* resolve with first resolution value.
*
* This is a convenience method which uses the `Queue` internally to
* schedule all jobs while limiting concurrency to ensure no more than
* `$concurrency` jobs ever run at once. It will return a promise which
* resolves with the result of the first job on success and will then try
* to `cancel()` all outstanding jobs.
*
* ```php
* $browser = new React\Http\Browser();
*
* $promise = Queue::any(3, $urls, function ($url) use ($browser) {
* return $browser->get($url);
* });
*
* $promise->then(function (ResponseInterface $response) {
* echo 'First response: ' . $response->getBody() . PHP_EOL;
* });
* ```
*
* If all of the jobs fail, it will reject the resulting promise. Similarly,
* calling `cancel()` on the resulting promise will try to cancel all
* outstanding jobs. See [promises](#promises) and
* [cancellation](#cancellation) for details.
*
* The `$concurrency` parameter sets a new soft limit for the maximum number
* of jobs to handle concurrently. Finding a good concurrency limit depends
* on your particular use case. It's common to limit concurrency to a rather
* small value, as doing more than a dozen of things at once may easily
* overwhelm the receiving side. Using a `1` value will ensure that all jobs
* are processed one after another, effectively creating a "waterfall" of
* jobs. Using a value less than 1 will reject with an
* `InvalidArgumentException` without processing any jobs.
*
* ```php
* // handle up to 10 jobs concurrently
* $promise = Queue::any(10, $jobs, $handler);
* ```
*
* ```php
* // handle each job after another without concurrency (waterfall)
* $promise = Queue::any(1, $jobs, $handler);
* ```
*
* The `$jobs` parameter must be an array with all jobs to process. Each
* value in this array will be passed to the `$handler` to start one job.
* The array keys have no effect, the promise will simply resolve with the
* job results of the first successful job as returned by the `$handler`.
* If this array is empty, this method will reject without processing any
* jobs.
*
* The `$handler` parameter must be a valid callable that accepts your job
* parameters, invokes the appropriate operation and returns a Promise as a
* placeholder for its future result. If the given argument is not a valid
* callable, this method will reject with an `InvalidArgumentExceptionn`
* without processing any jobs.
*
* ```php
* // using a Closure as handler is usually recommended
* $promise = Queue::any(10, $jobs, function ($url) use ($browser) {
* return $browser->get($url);
* });
* ```
*
* ```php
* // accepts any callable, so PHP's array notation is also supported
* $promise = Queue::any(10, $jobs, array($browser, 'get'));
* ```
*
* @param int $concurrency concurrency soft limit
* @param array $jobs
* @param callable $handler
* @return PromiseInterface Returns a Promise<mixed> which resolves with a single resolution value
* or rejects when all of the operations reject.
*/
public static function any($concurrency, array $jobs, $handler)
{
// explicitly reject with empty jobs (https://github.com/reactphp/promise/pull/34)
if (!$jobs) {
return Promise\reject(new \UnderflowException('No jobs given'));
}
try {
// limit number of concurrent operations
$q = new self($concurrency, null, $handler);
} catch (\InvalidArgumentException $e) {
// reject if $concurrency or $handler is invalid
return Promise\reject($e);
}
// try invoking all operations and automatically queue excessive ones
$promises = array_map($q, $jobs);
return new Promise\Promise(function ($resolve, $reject) use ($promises) {
Promise\any($promises)->then(function ($result) use ($promises, $resolve) {
// cancel all pending promises if a single result is ready
foreach (array_reverse($promises) as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
// resolve with original resolution value
$resolve($result);
}, $reject);
}, function () use ($promises) {
// cancel all pending promises on cancellation
foreach (array_reverse($promises) as $promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
}
});
}
/**
* Instantiates a new queue object.
*
* You can create any number of queues, for example when you want to apply
* different limits to different kind of operations.
*
* The `$concurrency` parameter sets a new soft limit for the maximum number
* of jobs to handle concurrently. Finding a good concurrency limit depends
* on your particular use case. It's common to limit concurrency to a rather
* small value, as doing more than a dozen of things at once may easily
* overwhelm the receiving side.
*
* The `$limit` parameter sets a new hard limit on how many jobs may be
* outstanding (kept in memory) at once. Depending on your particular use
* case, it's usually safe to keep a few hundreds or thousands of jobs in
* memory. If you do not want to apply an upper limit, you can pass a `null`
* value which is semantically more meaningful than passing a big number.
*
* ```php
* // handle up to 10 jobs concurrently, but keep no more than 1000 in memory
* $q = new Queue(10, 1000, $handler);
* ```
*
* ```php
* // handle up to 10 jobs concurrently, do not limit queue size
* $q = new Queue(10, null, $handler);
* ```
*
* ```php
* // handle up to 10 jobs concurrently, reject all further jobs
* $q = new Queue(10, 10, $handler);
* ```
*
* The `$handler` parameter must be a valid callable that accepts your job
* parameters, invokes the appropriate operation and returns a Promise as a
* placeholder for its future result.
*
* ```php
* // using a Closure as handler is usually recommended
* $q = new Queue(10, null, function ($url) use ($browser) {
* return $browser->get($url);
* });
* ```
*
* ```php
* // PHP's array callable as handler is also supported
* $q = new Queue(10, null, array($browser, 'get'));
* ```
*
* @param int $concurrency concurrency soft limit
* @param int|null $limit queue hard limit or NULL=unlimited
* @param callable $handler
* @throws \InvalidArgumentException
*/
public function __construct($concurrency, $limit, $handler)
{
if ($concurrency < 1 || ($limit !== null && ($limit < 1 || $concurrency > $limit))) {
throw new \InvalidArgumentException('Invalid limit given');
}
if (!is_callable($handler)) {
throw new \InvalidArgumentException('Invalid handler given');
}
$this->concurrency = $concurrency;
$this->limit = $limit;
$this->handler = $handler;
}
/**
* The Queue instance is invokable, so that invoking `$q(...$args)` will
* actually be forwarded as `$handler(...$args)` as given in the
* `$handler` argument when concurrency is still below limits.
*
* Each operation may take some time to complete, but due to its async nature you
* can actually start any number of (queued) operations. Once the concurrency limit
* is reached, this invocation will simply be queued and this will return a pending
* promise which will start the actual operation once another operation is
* completed. This means that this is handled entirely transparently and you do not
* need to worry about this concurrency limit yourself.
*
* @return \React\Promise\PromiseInterface
*/
public function __invoke()
{
// happy path: simply invoke handler if we're below concurrency limit
if ($this->pending < $this->concurrency) {
++$this->pending;
// invoke handler and await its resolution before invoking next queued job
return $this->await(
call_user_func_array($this->handler, func_get_args())
);
}
// we're currently above concurrency limit, make sure we do not exceed maximum queue limit
if ($this->limit !== null && $this->count() >= $this->limit) {
return Promise\reject(new \OverflowException('Maximum queue limit of ' . $this->limit . ' exceeded'));
}
// if we reach this point, then this job will need to be queued
// get next queue position
$queue =& $this->queue;
$queue[] = null;
end($queue);
$id = key($queue);
$deferred = new Deferred(function ($_, $reject) use (&$queue, $id, &$deferred) {
// forward cancellation to pending operation if it is currently executing
if (isset($deferred->pending) && $deferred->pending instanceof PromiseInterface && \method_exists($deferred->pending, 'cancel')) {
$deferred->pending->cancel();
}
unset($deferred->pending);
if (isset($deferred->args)) {
// queued promise cancelled before its handler is invoked
// remove from queue and reject explicitly
unset($queue[$id], $deferred->args);
$reject(new \RuntimeException('Cancelled queued job before processing started'));
}
});
// queue job to process if number of pending jobs is below concurrency limit again
$deferred->args = func_get_args();
$queue[$id] = $deferred;
return $deferred->promise();
}
#[\ReturnTypeWillChange]
public function count()
{
return $this->pending + count($this->queue);
}
/**
* @internal
*/
public function await(PromiseInterface $promise)
{
$that = $this;
return $promise->then(function ($result) use ($that) {
$that->processQueue();
return $result;
}, 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 job waiting
if (--$this->pending >= $this->concurrency || !$this->queue) {
return;
}
/* @var $deferred Deferred */
$deferred = reset($this->queue);
unset($this->queue[key($this->queue)]);
// once number of pending jobs is below concurrency limit again:
// await this situation, invoke handler and await its resolution before invoking next queued job
++$this->pending;
$promise = call_user_func_array($this->handler, $deferred->args);
$deferred->pending = $promise;
unset($deferred->args);
// invoke handler and await its resolution before invoking next queued job
$this->await($promise)->then(
function ($result) use ($deferred) {
unset($deferred->pending);
$deferred->resolve($result);
},
function ($e) use ($deferred) {
unset($deferred->pending);
$deferred->reject($e);
}
);
}
}

View File

@ -0,0 +1,8 @@
language: php
php:
- 5.4
- 5.3
before_script:
- composer install --dev --prefer-source --no-interaction
script:
- phpunit --coverage-text

50
vendor/clue/redis-protocol/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,50 @@
# Changelog
## 0.3.1 (2017-06-06)
* Fix: Fix server-side parsing of legacy inline protocol when multiple requests are processed at once
(#12 by @kelunik and #13 by @clue)
## 0.3.0 (2014-01-27)
* Feature: Add dedicated and faster `RequestParser` that also support the old
inline request protocol.
* Feature: Message serialization can now be handled directly by the Serializer
again without having to construct the appropriate model first.
* BC break: The `Factory` now has two distinct methods to create parsers:
* `createResponseParser()` for a client-side library
* `createRequestParser()` for a server-side library / testing framework
* BC break: Simplified parser API, now `pushIncoming()` returns an array of all
parsed message models.
* BC break: The signature for getting a serialized message from a model was
changed and now requires a Serializer passed:
```php
ModelInterface::getMessageSerialized($serializer)
```
* Many, many performance improvements
## 0.2.0 (2014-01-21)
* Re-organize the whole API into dedicated
* `Parser` (protocol reader) and
* `Serializer` (protocol writer) sub-namespaces. (#4)
* Use of the factory has now been unified:
```php
$factory = new Clue\Redis\Protocol\Factory();
$parser = $factory->createParser();
$serializer = $factory->createSerializer();
```
* Add a dedicated `Model` for each type of reply. Among others, this now allows
you to distinguish a single line `StatusReply` from a binary-safe `BulkReply`. (#2)
* Fix parsing binary values and do not trip over trailing/leading whitespace. (#4)
* Improve parser and serializer performance by up to 20%. (#4)
## 0.1.0 (2013-09-10)
* First tagged release

139
vendor/clue/redis-protocol/README.md vendored Normal file
View File

@ -0,0 +1,139 @@
# clue/redis-protocol [![Build Status](https://travis-ci.org/clue/php-redis-protocol.png?branch=master)](https://travis-ci.org/clue/php-redis-protocol)
A streaming redis protocol parser and serializer written in PHP
This parser and serializer implementation allows you to parse redis protocol
messages into native PHP values and vice-versa. This is usually needed by a
redis client implementation which also handles the connection socket.
To re-iterate: This is *not* a redis client implementation. This is a protocol
implementation that is usually used by a redis client implementation. If you're
looking for an easy way to build your own client implementation, then this is
for you. If you merely want to connect to a redis server and issue some
commands, you're probably better off using one of the existing client
implementations.
**Table of contents**
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [Factory](#factory)
* [Parser](#parser)
* [Model](#model)
* [Serializer](#serializer)
* [Install](#install)
* [License](#license)
## Quickstart example
```php
use Clue\Redis\Protocol;
$factory = new Protocol\Factory();
$parser = $factory->createResponseParser();
$serializer = $factory->createSerializer();
$fp = fsockopen('tcp://localhost', 6379);
fwrite($fp, $serializer->getRequestMessage('SET', array('name', 'value')));
fwrite($fp, $serializer->getRequestMessage('GET', array('name')));
// the commands are pipelined, so this may parse multiple responses
$models = $parser->pushIncoming(fread($fp, 4096));
$reply1 = array_shift($models);
$reply2 = array_shift($models);
var_dump($reply1->getValueNative()); // string(2) "OK"
var_dump($reply2->getValueNative()); // string(5) "value"
```
## Usage
### Factory
The factory helps with instantiating the *right* parser and serializer.
Eventually the *best* available implementation will be chosen depending on your
installed extensions. You're also free to instantiate them directly, but this
will lock you down on a given implementation (which could be okay depending on
your use-case).
### Parser
The library includes a streaming redis protocol parser. As such, it can safely
parse redis protocol messages and work with an incomplete data stream. For this,
each included parser implements a single method
`ParserInterface::pushIncoming($chunk)`.
* The `ResponseParser` is what most redis client implementation would want to
use in order to parse incoming response messages from a redis server instance.
* The `RequestParser` can be used to test messages coming from a redis client or
even to implement a redis server.
* The `MessageBuffer` decorates either of the available parsers and merely
offers some helper methods in order to work with single messages:
* `hasIncomingModel()` to check if there's a complete message in the pipeline
* `popIncomingModel()` to extract a complete message from the incoming queue.
### Model
Each message (response as well as request) is represented by a model
implementing the `ModelInterface` that has two methods:
* `getValueNative()` returns the wrapped value.
* `getMessageSerialized($serializer)` returns the serialized protocol messages
that will be sent over the wire.
These models are very lightweight and add little overhead. They help keeping the
code organized and also provide a means to distinguish a single line
`StatusReply` from a binary-safe `BulkReply`.
The parser always returns models. Models can also be instantiated directly:
```php
$model = new Model\IntegerReply(123);
var_dump($model->getValueNative()); // int(123)
var_dump($model->getMessageSerialized($serializer)); // string(6) ":123\r\n"
```
### Serializer
The serializer is responsible for creating serialized messages and the
corresponing message models to be sent across the wire.
```php
$message = $serializer->getRequestMessage('ping');
var_dump($message); // string(14) "$1\r\n*4\r\nping\r\n"
$message = $serializer->getRequestMessage('set', array('key', 'value'));
var_dump($message); // string(33) "$3\r\n*3\r\nset\r\n*3\r\nkey\r\n*5\r\nvalue\r\n"
$model = $serializer->createRequestModel('get', array('key'));
var_dump($model->getCommand()); // string(3) "get"
var_dump($model->getArgs()); // array(1) { string(3) "key" }
var_dump($model->getValueNative()); // array(2) { string(3) "GET", string(3) "key" }
$model = $serializer->createReplyModel(array('mixed', 12, array('value')));
assert($model implement Model\MultiBulkReply);
```
## Install
It's very unlikely you'll want to use this protocol parser standalone.
It should be added as a dependency to your redis client implementation instead.
The recommended way to install this library is [through Composer](https://getcomposer.org).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This will install the latest supported version:
```bash
$ composer require clue/redis-protocol:^0.3.1
```
More details and upgrade guides can be found in the [CHANGELOG](CHANGELOG.md).
## License
Its parser and serializer originally used to be based on
[jpd/redisent](https://github.com/jdp/redisent), which is released under the ISC
license, copyright (c) 2009-2012 Justin Poliey <justin@getglue.com>.
Other than that, this library is MIT licensed.

View File

@ -0,0 +1,19 @@
{
"name": "clue/redis-protocol",
"description": "A streaming redis wire protocol parser and serializer implementation in PHP",
"keywords": ["streaming", "redis", "protocol", "parser", "serializer"],
"homepage": "https://github.com/clue/php-redis-protocol",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"require": {
"php": ">=5.3"
},
"autoload": {
"psr-0": { "Clue\\Redis\\Protocol": "src" }
}
}

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="Redis Protocol Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,51 @@
<?php
namespace Clue\Redis\Protocol;
use Clue\Redis\Protocol\Parser\ParserInterface;
use Clue\Redis\Protocol\Parser\ResponseParser;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
use Clue\Redis\Protocol\Serializer\RecursiveSerializer;
use Clue\Redis\Protocol\Parser\RequestParser;
/**
* Provides factory methods used to instantiate the best available protocol implementation
*/
class Factory
{
/**
* instantiate the best available protocol response parser implementation
*
* This is the parser every redis client implementation should use in order
* to parse incoming response messages from a redis server.
*
* @return ParserInterface
*/
public function createResponseParser()
{
return new ResponseParser();
}
/**
* instantiate the best available protocol request parser implementation
*
* This is most useful for a redis server implementation which needs to
* process client requests.
*
* @return ParserInterface
*/
public function createRequestParser()
{
return new RequestParser();
}
/**
* instantiate the best available protocol serializer implementation
*
* @return SerializerInterface
*/
public function createSerializer()
{
return new RecursiveSerializer();
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Clue\Redis\Protocol\Model;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
class BulkReply implements ModelInterface
{
private $value;
/**
* create bulk reply (string reply)
*
* @param string|null $data
*/
public function __construct($value)
{
if ($value !== null) {
$value = (string)$value;
}
$this->value = $value;
}
public function getValueNative()
{
return $this->value;
}
public function getMessageSerialized(SerializerInterface $serializer)
{
return $serializer->getBulkMessage($this->value);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Clue\Redis\Protocol\Model;
use Exception;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
/**
*
* @link http://redis.io/topics/protocol#status-reply
*/
class ErrorReply extends Exception implements ModelInterface
{
/**
* create error status reply (single line error message)
*
* @param string|ErrorReplyException $message
* @return string
*/
public function __construct($message, $code = 0, $previous = null)
{
parent::__construct($message, $code, $previous);
}
public function getValueNative()
{
return $this->getMessage();
}
public function getMessageSerialized(SerializerInterface $serializer)
{
return $serializer->getErrorMessage($this->getMessage());
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Clue\Redis\Protocol\Model;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
class IntegerReply implements ModelInterface
{
private $value;
/**
* create integer reply
*
* @param int $data
*/
public function __construct($value)
{
$this->value = (int)$value;
}
public function getValueNative()
{
return $this->value;
}
public function getMessageSerialized(SerializerInterface $serializer)
{
return $serializer->getIntegerMessage($this->value);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Clue\Redis\Protocol\Model;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
interface ModelInterface
{
/**
* Returns value of this model as a native representation for PHP
*
* @return mixed
*/
public function getValueNative();
/**
* Returns the serialized representation of this protocol message
*
* @param SerializerInterface $serializer;
* @return string
*/
public function getMessageSerialized(SerializerInterface $serializer);
}

View File

@ -0,0 +1,100 @@
<?php
namespace Clue\Redis\Protocol\Model;
use InvalidArgumentException;
use UnexpectedValueException;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
class MultiBulkReply implements ModelInterface
{
/**
* @var array|null
*/
private $data;
/**
* create multi bulk reply (an array of other replies, usually bulk replies)
*
* @param array|null $data
* @throws InvalidArgumentException
*/
public function __construct(array $data = null)
{
$this->data = $data;
}
public function getValueNative()
{
if ($this->data === null) {
return null;
}
$ret = array();
foreach ($this->data as $one) {
if ($one instanceof ModelInterface) {
$ret []= $one->getValueNative();
} else {
$ret []= $one;
}
}
return $ret;
}
public function getMessageSerialized(SerializerInterface $serializer)
{
return $serializer->getMultiBulkMessage($this->data);
}
/**
* Checks whether this model represents a valid unified request protocol message
*
* The new unified protocol was introduced in Redis 1.2, but it became the
* standard way for talking with the Redis server in Redis 2.0. The unified
* request protocol is what Redis already uses in replies in order to send
* list of items to clients, and is called a Multi Bulk Reply.
*
* @return boolean
* @link http://redis.io/topics/protocol
*/
public function isRequest()
{
if (!$this->data) {
return false;
}
foreach ($this->data as $one) {
if (!($one instanceof BulkReply) && !is_string($one)) {
return false;
}
}
return true;
}
public function getRequestModel()
{
if (!$this->data) {
throw new UnexpectedValueException('Null-multi-bulk message can not be represented as a request, must contain string/bulk values');
}
$command = null;
$args = array();
foreach ($this->data as $one) {
if ($one instanceof BulkReply) {
$one = $one->getValueNative();
} elseif (!is_string($one)) {
throw new UnexpectedValueException('Message can not be represented as a request, must only contain string/bulk values');
}
if ($command === null) {
$command = $one;
} else {
$args []= $one;
}
}
return new Request($command, $args);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Clue\Redis\Protocol\Model;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Model\BulkReply;
use Clue\Redis\Protocol\Model\MultiBulkReply;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
class Request implements ModelInterface
{
private $command;
private $args;
public function __construct($command, array $args = array())
{
$this->command = $command;
$this->args = $args;
}
public function getCommand()
{
return $this->command;
}
public function getArgs()
{
return $this->args;
}
public function getReplyModel()
{
$models = array(new BulkReply($this->command));
foreach ($this->args as $arg) {
$models []= new BulkReply($arg);
}
return new MultiBulkReply($models);
}
public function getValueNative()
{
$ret = $this->args;
array_unshift($ret, $this->command);
return $ret;
}
public function getMessageSerialized(SerializerInterface $serializer)
{
return $serializer->getRequestMessage($this->command, $this->args);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Clue\Redis\Protocol\Model;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
/**
*
* @link http://redis.io/topics/protocol#status-reply
*/
class StatusReply implements ModelInterface
{
private $message;
/**
* create status reply (single line message)
*
* @param string|Status $message
* @return string
*/
public function __construct($message)
{
$this->message = $message;
}
public function getValueNative()
{
return $this->message;
}
public function getMessageSerialized(SerializerInterface $serializer)
{
return $serializer->getStatusMessage($this->message);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace Clue\Redis\Protocol\Parser;
use UnderflowException;
class MessageBuffer implements ParserInterface
{
private $parser;
private $incomingQueue = array();
public function __construct(ParserInterface $parser)
{
$this->parser = $parser;
}
public function popIncomingModel()
{
if (!$this->incomingQueue) {
throw new UnderflowException('Incoming message queue is empty');
}
return array_shift($this->incomingQueue);
}
public function hasIncomingModel()
{
return ($this->incomingQueue) ? true : false;
}
public function pushIncoming($data)
{
$ret = $this->parser->pushIncoming($data);
foreach ($ret as $one) {
$this->incomingQueue []= $one;
}
return $ret;
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Clue\Redis\Protocol\Parser;
use UnexpectedValueException;
class ParserException extends UnexpectedValueException
{
}

View File

@ -0,0 +1,28 @@
<?php
namespace Clue\Redis\Protocol\Parser;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Parser\ParserException;
interface ParserInterface
{
/**
* push a chunk of the redis protocol message into the buffer and parse
*
* You can push any number of bytes of a redis protocol message into the
* parser and it will try to parse messages from its data stream. So you can
* pass data directly from your socket stream and the parser will return the
* right amount of message model objects for you.
*
* If you pass an incomplete message, expect it to return an empty array. If
* your incomplete message is split to across multiple chunks, the parsed
* message model will be returned once the parser has sufficient data.
*
* @param string $dataChunk
* @return ModelInterface[] 0+ message models
* @throws ParserException if the message can not be parsed
* @see self::popIncomingModel()
*/
public function pushIncoming($dataChunk);
}

View File

@ -0,0 +1,125 @@
<?php
namespace Clue\Redis\Protocol\Parser;
use Clue\Redis\Protocol\Parser\ParserException;
use Clue\Redis\Protocol\Model\Request;
class RequestParser implements ParserInterface
{
const CRLF = "\r\n";
private $incomingBuffer = '';
private $incomingOffset = 0;
public function pushIncoming($dataChunk)
{
$this->incomingBuffer .= $dataChunk;
$parsed = array();
do {
$saved = $this->incomingOffset;
$message = $this->readRequest();
if ($message === null) {
// restore previous position for next parsing attempt
$this->incomingOffset = $saved;
break;
}
if ($message !== false) {
$parsed []= $message;
}
} while($this->incomingBuffer !== '');
if ($this->incomingOffset !== 0) {
$this->incomingBuffer = (string)substr($this->incomingBuffer, $this->incomingOffset);
$this->incomingOffset = 0;
}
return $parsed;
}
/**
* try to parse request from incoming buffer
*
* @throws ParserException if the incoming buffer is invalid
* @return Request|null
*/
private function readRequest()
{
$crlf = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
if ($crlf === false) {
return null;
}
// line starts with a multi-bulk header "*"
if (isset($this->incomingBuffer[$this->incomingOffset]) && $this->incomingBuffer[$this->incomingOffset] === '*') {
$line = substr($this->incomingBuffer, $this->incomingOffset + 1, $crlf - $this->incomingOffset + 1);
$this->incomingOffset = $crlf + 2;
$count = (int)$line;
if ($count <= 0) {
return false;
}
$command = null;
$args = array();
for ($i = 0; $i < $count; ++$i) {
$sub = $this->readBulk();
if ($sub === null) {
return null;
}
if ($command === null) {
$command = $sub;
} else {
$args []= $sub;
}
}
return new Request($command, $args);
}
// parse an old inline request instead
$line = substr($this->incomingBuffer, $this->incomingOffset, $crlf - $this->incomingOffset);
$this->incomingOffset = $crlf + 2;
$args = preg_split('/ +/', trim($line, ' '));
$command = array_shift($args);
if ($command === '') {
return false;
}
return new Request($command, $args);
}
private function readBulk()
{
$crlf = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
if ($crlf === false) {
return null;
}
// line has to start with a bulk header "$"
if (!isset($this->incomingBuffer[$this->incomingOffset]) || $this->incomingBuffer[$this->incomingOffset] !== '$') {
throw new ParserException('ERR Protocol error: expected \'$\', got \'' . substr($this->incomingBuffer, $this->incomingOffset, 1) . '\'');
}
$line = substr($this->incomingBuffer, $this->incomingOffset + 1, $crlf - $this->incomingOffset + 1);
$this->incomingOffset = $crlf + 2;
$size = (int)$line;
if ($size < 0) {
throw new ParserException('ERR Protocol error: invalid bulk length');
}
if (!isset($this->incomingBuffer[$this->incomingOffset + $size + 1])) {
// check enough bytes + crlf are buffered
return null;
}
$ret = substr($this->incomingBuffer, $this->incomingOffset, $size);
$this->incomingOffset += $size + 2;
return $ret;
}
}

View File

@ -0,0 +1,151 @@
<?php
namespace Clue\Redis\Protocol\Parser;
use Clue\Redis\Protocol\Parser\ParserInterface;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Model\BulkReply;
use Clue\Redis\Protocol\Model\ErrorReply;
use Clue\Redis\Protocol\Model\IntegerReply;
use Clue\Redis\Protocol\Model\MultiBulkReply;
use Clue\Redis\Protocol\Model\StatusReply;
use Clue\Redis\Protocol\Parser\ParserException;
/**
* Simple recursive redis wire protocol parser
*
* Heavily influenced by blocking parser implementation from jpd/redisent.
*
* @link https://github.com/jdp/redisent
* @link http://redis.io/topics/protocol
*/
class ResponseParser implements ParserInterface
{
const CRLF = "\r\n";
private $incomingBuffer = '';
private $incomingOffset = 0;
public function pushIncoming($dataChunk)
{
$this->incomingBuffer .= $dataChunk;
return $this->tryParsingIncomingMessages();
}
private function tryParsingIncomingMessages()
{
$messages = array();
do {
$message = $this->readResponse();
if ($message === null) {
// restore previous position for next parsing attempt
$this->incomingOffset = 0;
break;
}
$messages []= $message;
$this->incomingBuffer = (string)substr($this->incomingBuffer, $this->incomingOffset);
$this->incomingOffset = 0;
} while($this->incomingBuffer !== '');
return $messages;
}
private function readLine()
{
$pos = strpos($this->incomingBuffer, "\r\n", $this->incomingOffset);
if ($pos === false) {
return null;
}
$ret = (string)substr($this->incomingBuffer, $this->incomingOffset, $pos - $this->incomingOffset);
$this->incomingOffset = $pos + 2;
return $ret;
}
private function readLength($len)
{
$ret = substr($this->incomingBuffer, $this->incomingOffset, $len);
if (strlen($ret) !== $len) {
return null;
}
$this->incomingOffset += $len;
return $ret;
}
/**
* try to parse response from incoming buffer
*
* ripped from jdp/redisent, with some minor modifications to read from
* the incoming buffer instead of issuing a blocking fread on a stream
*
* @throws ParserException if the incoming buffer is invalid
* @return ModelInterface|null
* @link https://github.com/jdp/redisent
*/
private function readResponse()
{
/* Parse the response based on the reply identifier */
$reply = $this->readLine();
if ($reply === null) {
return null;
}
switch (substr($reply, 0, 1)) {
/* Error reply */
case '-':
$response = new ErrorReply(substr($reply, 1));
break;
/* Inline reply */
case '+':
$response = new StatusReply(substr($reply, 1));
break;
/* Bulk reply */
case '$':
$size = (int)substr($reply, 1);
if ($size === -1) {
return new BulkReply(null);
}
$data = $this->readLength($size);
if ($data === null) {
return null;
}
if ($this->readLength(2) === null) { /* discard crlf */
return null;
}
$response = new BulkReply($data);
break;
/* Multi-bulk reply */
case '*':
$count = (int)substr($reply, 1);
if ($count === -1) {
return new MultiBulkReply(null);
}
$response = array();
for ($i = 0; $i < $count; $i++) {
$sub = $this->readResponse();
if ($sub === null) {
return null;
}
$response []= $sub;
}
$response = new MultiBulkReply($response);
break;
/* Integer reply */
case ':':
$response = new IntegerReply(substr($reply, 1));
break;
default:
throw new ParserException('Invalid message can not be parsed: "' . $reply . '"');
break;
}
/* Party on */
return $response;
}
}

View File

@ -0,0 +1,111 @@
<?php
namespace Clue\Redis\Protocol\Serializer;
use Clue\Redis\Protocol\Model\StatusReply;
use InvalidArgumentException;
use Exception;
use Clue\Redis\Protocol\Model\BulkReply;
use Clue\Redis\Protocol\Model\IntegerReply;
use Clue\Redis\Protocol\Model\ErrorReply;
use Clue\Redis\Protocol\Model\MultiBulkReply;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Model\Request;
class RecursiveSerializer implements SerializerInterface
{
const CRLF = "\r\n";
public function getRequestMessage($command, array $args = array())
{
$data = '*' . (count($args) + 1) . "\r\n$" . strlen($command) . "\r\n" . $command . "\r\n";
foreach ($args as $arg) {
$data .= '$' . strlen($arg) . "\r\n" . $arg . "\r\n";
}
return $data;
}
public function createRequestModel($command, array $args = array())
{
return new Request($command, $args);
}
public function getReplyMessage($data)
{
if (is_string($data) || $data === null) {
return $this->getBulkMessage($data);
} else if (is_int($data) || is_float($data) || is_bool($data)) {
return $this->getIntegerMessage($data);
} else if ($data instanceof Exception) {
return $this->getErrorMessage($data->getMessage());
} else if (is_array($data)) {
return $this->getMultiBulkMessage($data);
} else {
throw new InvalidArgumentException('Invalid data type passed for serialization');
}
}
public function createReplyModel($data)
{
if (is_string($data) || $data === null) {
return new BulkReply($data);
} else if (is_int($data) || is_float($data) || is_bool($data)) {
return new IntegerReply($data);
} else if ($data instanceof Exception) {
return new ErrorReply($data->getMessage());
} else if (is_array($data)) {
$models = array();
foreach ($data as $one) {
$models []= $this->createReplyModel($one);
}
return new MultiBulkReply($models);
} else {
throw new InvalidArgumentException('Invalid data type passed for serialization');
}
}
public function getBulkMessage($data)
{
if ($data === null) {
/* null bulk reply */
return '$-1' . self::CRLF;
}
/* bulk reply */
return '$' . strlen($data) . self::CRLF . $data . self::CRLF;
}
public function getErrorMessage($data)
{
/* error status reply */
return '-' . $data . self::CRLF;
}
public function getIntegerMessage($data)
{
return ':' . (int)$data . self::CRLF;
}
public function getMultiBulkMessage($data)
{
if ($data === null) {
/* null multi bulk reply */
return '*-1' . self::CRLF;
}
/* multi bulk reply */
$ret = '*' . count($data) . self::CRLF;
foreach ($data as $one) {
if ($one instanceof ModelInterface) {
$ret .= $one->getMessageSerialized($this);
} else {
$ret .= $this->getReplyMessage($one);
}
}
return $ret;
}
public function getStatusMessage($data)
{
/* status reply */
return '+' . $data . self::CRLF;
}
}

View File

@ -0,0 +1,83 @@
<?php
namespace Clue\Redis\Protocol\Serializer;
use Clue\Redis\Protocol\Model\ErrorReplyException;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Model\MultiBulkReply;
interface SerializerInterface
{
/**
* create a serialized unified request protocol message
*
* This is the *one* method most redis client libraries will likely want to
* use in order to send a serialized message (a request) over the* wire to
* your redis server instance.
*
* This method should be used in favor of constructing a request model and
* then serializing it. While its effect might be equivalent, this method
* is likely to (i.e. it /could/) provide a faster implementation.
*
* @param string $command
* @param array $args
* @return string
* @see self::createRequestMessage()
*/
public function getRequestMessage($command, array $args = array());
/**
* create a unified request protocol message model
*
* @param string $command
* @param array $args
* @return MultiBulkReply
*/
public function createRequestModel($command, array $args = array());
/**
* create a serialized unified protocol reply message
*
* This is most useful for a redis server implementation which needs to
* process client requests and send resulting reply messages.
*
* This method does its best to guess to right reply type and then returns
* a serialized version of the message. It follows the "redis to lua
* conversion table" (see link) which means most basic types can be mapped
* as is.
*
* This method should be used in favor of constructing a reply model and
* then serializing it. While its effect might be equivalent, this method
* is likely to (i.e. it /could/) provide a faster implementation.
*
* Note however, you may still want to explicitly create a nested reply
* model hierarchy if you need more control over the serialized message. For
* instance, a null value will always be returned as a Null-Bulk-Reply, so
* there's no way to express a Null-Multi-Bulk-Reply, unless you construct
* it explicitly.
*
* @param mixed $data
* @return string
* @see self::createReplyModel()
* @link http://redis.io/commands/eval
*/
public function getReplyMessage($data);
/**
* create response message by determining datatype from given argument
*
* @param mixed $data
* @return ModelInterface
*/
public function createReplyModel($data);
public function getBulkMessage($data);
public function getErrorMessage($data);
public function getIntegerMessage($data);
public function getMultiBulkMessage($data);
public function getStatusMessage($data);
}

268
vendor/clue/redis-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,268 @@
# Changelog
## 2.6.0 (2022-05-09)
* Feature: Support PHP 8.1 release.
(#119 by @clue)
* Improve documentation and CI configuration.
(#123 and #125 by @SimonFrings)
## 2.5.0 (2021-08-31)
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop) and new Socket API.
(#114 and #115 by @SimonFrings)
```php
// old (still supported)
$factory = new Clue\React\Redis\Factory($loop);
// new (using default loop)
$factory = new Clue\React\Redis\Factory();
```
* Feature: Improve error reporting, include Redis URI and socket error codes in all connection errors.
(#116 by @clue)
* Documentation improvements and updated examples.
(#117 by @clue, #112 by @Nyholm and #113 by @PaulRotmann)
* Improve test suite and use GitHub actions for continuous integration (CI).
(#111 by @SimonFrings)
## 2.4.0 (2020-09-25)
* Fix: Fix dangling timer when lazy connection closes with pending commands.
(#105 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix.
(#96 and #97 by @clue and #99, #101 and #104 by @SimonFrings)
## 2.3.0 (2019-03-11)
* Feature: Add new `createLazyClient()` method to connect only on demand and
implement "idle" timeout to close underlying connection when unused.
(#87 and #88 by @clue and #82 by @WyriHaximus)
```php
$client = $factory->createLazyClient('redis://localhost:6379');
$client->incr('hello');
$client->end();
```
* Feature: Support cancellation of pending connection attempts.
(#85 by @clue)
```php
$promise = $factory->createClient($redisUri);
$loop->addTimer(3.0, function () use ($promise) {
$promise->cancel();
});
```
* Feature: Support connection timeouts.
(#86 by @clue)
```php
$factory->createClient('localhost?timeout=0.5');
```
* Feature: Improve Exception messages for connection issues.
(#89 by @clue)
```php
$factory->createClient('redis://localhost:6379')->then(
function (Client $client) {
// client connected (and authenticated)
},
function (Exception $e) {
// an error occurred while trying to connect (or authenticate) client
echo $e->getMessage() . PHP_EOL;
if ($e->getPrevious()) {
echo $e->getPrevious()->getMessage() . PHP_EOL;
}
}
);
```
* Improve test suite structure and add forward compatibility with PHPUnit 7 and PHPUnit 6
and test against PHP 7.1, 7.2, and 7.3 on TravisCI.
(#83 by @WyriHaximus and #84 by @clue)
* Improve documentation and update project homepage.
(#81 and #90 by @clue)
## 2.2.0 (2018-01-24)
* Feature: Support communication over Unix domain sockets (UDS)
(#70 by @clue)
```php
// new: now supports redis over Unix domain sockets (UDS)
$factory->createClient('redis+unix:///tmp/redis.sock');
```
## 2.1.0 (2017-09-25)
* Feature: Update Socket dependency to support hosts file on all platforms
(#66 by @clue)
This means that connecting to hosts such as `localhost` (and for example
those used for Docker containers) will now work as expected across all
platforms with no changes required:
```php
$factory->createClient('localhost');
```
## 2.0.0 (2017-09-20)
A major compatibility release to update this package to support all latest
ReactPHP components!
This update involves a minor BC break due to dropped support for legacy
versions. 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: Remove all deprecated APIs, default to `redis://` URI scheme
and drop legacy SocketClient in favor of new Socket component.
(#61 by @clue)
> All of this affects the `Factory` only, which is mostly considered
"advanced usage". If you're affected by this BC break, then it's
recommended to first update to the intermediary v1.2.0 release, which
allows you to use the `redis://` URI scheme and a standard
`ConnectorInterface` and then update to this version without causing a
BC break.
* BC break: Remove uneeded `data` event and support for advanced `MONITOR`
command for performance and consistency reasons and
remove underdocumented `isBusy()` method.
(#62, #63 and #64 by @clue)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 and EventLoop v1.0 and Evenement v3
(#65 by @clue)
## 1.2.0 (2017-09-19)
* Feature: Support `redis[s]://` URI scheme and deprecate legacy URIs
(#60 by @clue)
```php
$factory->createClient('redis://:secret@localhost:6379/4');
$factory->createClient('redis://localhost:6379?password=secret&db=4');
```
* Feature: Factory accepts Connector from Socket and deprecate legacy SocketClient
(#59 by @clue)
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($loop, array(
'dns' => '127.0.0.1',
'tcp' => array(
'bindto' => '192.168.10.1:0'
),
'tls' => array(
'verify_peer' => false,
'verify_peer_name' => false
)
));
$factory = new Factory($loop, $connector);
```
## 1.1.0 (2017-09-18)
* Feature: Update SocketClient dependency to latest version
(#58 by @clue)
* Improve test suite by adding PHPUnit to require-dev,
fix HHVM build for now again and ignore future HHVM build errors,
lock Travis distro so new defaults will not break the build and
skip functional integration tests by default
(#52, #53, #56 and #57 by @clue)
## 1.0.0 (2016-05-20)
* First stable release, now following SemVer
* BC break: Consistent public API, mark internal APIs as such
(#38 by @clue)
```php
// old
$client->on('data', function (MessageInterface $message, Client $client) {
// process an incoming message (raw message object)
});
// new
$client->on('data', function (MessageInterface $message) use ($client) {
// process an incoming message (raw message object)
});
```
> Contains no other changes, so it's actually fully compatible with the v0.5.2 release.
## 0.5.2 (2016-05-20)
* Fix: Do not send empty SELECT statement when no database has been given
(#35, #36 by @clue)
* Improve documentation, update dependencies and add first class support for PHP 7
## 0.5.1 (2015-01-12)
* Fix: Fix compatibility with react/promise v2.0 for monitor and PubSub commands.
(#28)
## 0.5.0 (2014-11-12)
* Feature: Support PubSub commands (P)(UN)SUBSCRIBE and watching for "message",
"subscribe" and "unsubscribe" events
(#24)
* Feature: Support MONITOR command and watching for "monitor" events
(#23)
* Improve documentation, update locked dependencies and add first class support for HHVM
(#25, #26 and others)
## 0.4.0 (2014-08-25)
* BC break: The `Client` class has been renamed to `StreamingClient`.
Added new `Client` interface.
(#18 and #19)
* BC break: Rename `message` event to `data`.
(#21)
* BC break: The `Factory` now accepts a `LoopInterface` as first argument.
(#22)
* Fix: The `close` event will be emitted once when invoking the `Client::close()`
method or when the underlying stream closes.
(#20)
* Refactored code, improved testability, extended test suite and better code coverage.
(#11, #18 and #20)
> Note: This is an intermediary release to ease upgrading to the imminent v0.5 release.
## 0.3.0 (2014-05-31)
* First tagged release
> Note: Starts at v0.3 because previous versions were not tagged. Leaving some
> room in case they're going to be needed in the future.
## 0.0.0 (2013-07-05)
* Initial concept

21
vendor/clue/redis-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Christian Lück
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.

660
vendor/clue/redis-react/README.md vendored Normal file
View File

@ -0,0 +1,660 @@
# clue/reactphp-redis
[![CI status](https://github.com/clue/reactphp-redis/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-redis/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/redis-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/redis-react)
Async [Redis](https://redis.io/) client implementation, built on top of [ReactPHP](https://reactphp.org/).
[Redis](https://redis.io/) is an open source, advanced, in-memory key-value database.
It offers a set of simple, atomic operations in order to work with its primitive data types.
Its lightweight design and fast operation makes it an ideal candidate for modern application stacks.
This library provides you a simple API to work with your Redis database from within PHP.
It enables you to set and query its data or use its PubSub topics to react to incoming events.
* **Async execution of Commands** -
Send any number of commands to Redis in parallel (automatic pipeline) and
process their responses as soon as results come in.
The Promise-based design provides a *sane* interface to working with async responses.
* **Event-driven core** -
Register your event handler callbacks to react to incoming events, such as an incoming PubSub message event.
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Future or custom commands and events require no changes to be supported.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested against versions as old as Redis v2.6 and newer.
**Table of Contents**
* [Support us](#support-us)
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [Commands](#commands)
* [Promises](#promises)
* [PubSub](#pubsub)
* [API](#api)
* [Factory](#factory)
* [createClient()](#createclient)
* [createLazyClient()](#createlazyclient)
* [Client](#client)
* [__call()](#__call)
* [end()](#end)
* [close()](#close)
* [error event](#error-event)
* [close event](#close-event)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Quickstart example
Once [installed](#install), you can use the following code to connect to your
local Redis server and send some requests:
```php
<?php
require __DIR__ . '/vendor/autoload.php';
$factory = new Clue\React\Redis\Factory();
$redis = $factory->createLazyClient('localhost:6379');
$redis->set('greeting', 'Hello world');
$redis->append('greeting', '!');
$redis->get('greeting')->then(function ($greeting) {
// Hello world!
echo $greeting . PHP_EOL;
});
$redis->incr('invocation')->then(function ($n) {
echo 'This is invocation #' . $n . PHP_EOL;
});
// end connection once all pending requests have been resolved
$redis->end();
```
See also the [examples](examples).
## Usage
### Commands
Most importantly, this project provides a [`Client`](#client) instance that
can be used to invoke all [Redis commands](https://redis.io/commands) (such as `GET`, `SET`, etc.).
```php
$redis->get($key);
$redis->set($key, $value);
$redis->exists($key);
$redis->expire($key, $seconds);
$redis->mget($key1, $key2, $key3);
$redis->multi();
$redis->exec();
$redis->publish($channel, $payload);
$redis->subscribe($channel);
$redis->ping();
$redis->select($database);
// many more…
```
Each method call matches the respective [Redis command](https://redis.io/commands).
For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get).
All [Redis commands](https://redis.io/commands) are automatically available as
public methods via the magic [`__call()` method](#__call).
Listing all available commands is out of scope here, please refer to the
[Redis command reference](https://redis.io/commands).
Any arguments passed to the method call will be forwarded as command arguments.
For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a
`SET name Alice` command. It's safe to pass integer arguments where applicable (for
example `$redis->expire($key, 60)`), but internally Redis requires all arguments to
always be coerced to string values.
Each of these commands supports async operation and returns a [Promise](#promises)
that eventually *fulfills* with its *results* on success or *rejects* with an
`Exception` on error. See also the following section about [promises](#promises)
for more details.
### Promises
Sending commands is async (non-blocking), so you can actually send multiple
commands in parallel.
Redis will respond to each command request with a response message, pending
commands will be pipelined automatically.
Sending commands uses a [Promise](https://github.com/reactphp/promise)-based
interface that makes it easy to react to when a command is completed
(i.e. either successfully fulfilled or rejected with an error):
```php
$redis->get($key)->then(function (?string $value) {
var_dump($value);
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
### PubSub
This library is commonly used to efficiently transport messages using Redis'
[Pub/Sub](https://redis.io/topics/pubsub) (Publish/Subscribe) channels. For
instance, this can be used to distribute single messages to a larger number
of subscribers (think horizontal scaling for chat-like applications) or as an
efficient message transport in distributed systems (microservice architecture).
The [`PUBLISH` command](https://redis.io/commands/publish) can be used to
send a message to all clients currently subscribed to a given channel:
```php
$channel = 'user';
$message = json_encode(array('id' => 10));
$redis->publish($channel, $message);
```
The [`SUBSCRIBE` command](https://redis.io/commands/subscribe) can be used to
subscribe to a channel and then receive incoming PubSub `message` events:
```php
$channel = 'user';
$redis->subscribe($channel);
$redis->on('message', function ($channel, $payload) {
// pubsub message received on given $channel
var_dump($channel, json_decode($payload));
});
```
Likewise, you can use the same client connection to subscribe to multiple
channels by simply executing this command multiple times:
```php
$redis->subscribe('user.register');
$redis->subscribe('user.join');
$redis->subscribe('user.leave');
```
Similarly, the [`PSUBSCRIBE` command](https://redis.io/commands/psubscribe) can
be used to subscribe to all channels matching a given pattern and then receive
all incoming PubSub messages with the `pmessage` event:
```php
$pattern = 'user.*';
$redis->psubscribe($pattern);
$redis->on('pmessage', function ($pattern, $channel, $payload) {
// pubsub message received matching given $pattern
var_dump($channel, json_decode($payload));
});
```
Once you're in a subscribed state, Redis no longer allows executing any other
commands on the same client connection. This is commonly worked around by simply
creating a second client connection and dedicating one client connection solely
for PubSub subscriptions and the other for all other commands.
The [`UNSUBSCRIBE` command](https://redis.io/commands/unsubscribe) and
[`PUNSUBSCRIBE` command](https://redis.io/commands/punsubscribe) can be used to
unsubscribe from active subscriptions if you're no longer interested in
receiving any further events for the given channel and pattern subscriptions
respectively:
```php
$redis->subscribe('user');
Loop::addTimer(60.0, function () use ($redis) {
$redis->unsubscribe('user');
});
```
Likewise, once you've unsubscribed the last channel and pattern, the client
connection is no longer in a subscribed state and you can issue any other
command over this client connection again.
Each of the above methods follows normal request-response semantics and return
a [`Promise`](#promises) to await successful subscriptions. Note that while
Redis allows a variable number of arguments for each of these commands, this
library is currently limited to single arguments for each of these methods in
order to match exactly one response to each command request. As an alternative,
the methods can simply be invoked multiple times with one argument each.
Additionally, can listen for the following PubSub events to get notifications
about subscribed/unsubscribed channels and patterns:
```php
$redis->on('subscribe', function ($channel, $total) {
// subscribed to given $channel
});
$redis->on('psubscribe', function ($pattern, $total) {
// subscribed to matching given $pattern
});
$redis->on('unsubscribe', function ($channel, $total) {
// unsubscribed from given $channel
});
$redis->on('punsubscribe', function ($pattern, $total) {
// unsubscribed from matching given $pattern
});
```
When using the [`createLazyClient()`](#createlazyclient) method, the `unsubscribe`
and `punsubscribe` events will be invoked automatically when the underlying
connection is lost. This gives you control over re-subscribing to the channels
and patterns as appropriate.
## API
### Factory
The `Factory` is responsible for creating your [`Client`](#client) instance.
```php
$factory = new Clue\React\Redis\Factory();
```
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.
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
)
));
$factory = new Clue\React\Redis\Factory(null, $connector);
```
#### createClient()
The `createClient(string $uri): PromiseInterface<Client,Exception>` method can be used to
create a new [`Client`](#client).
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
and optionally authenticating (AUTH) and selecting the right database (SELECT).
```php
$factory->createClient('localhost:6379')->then(
function (Client $redis) {
// client connected (and authenticated)
},
function (Exception $e) {
// an error occurred while trying to connect (or authenticate) client
}
);
```
The method returns a [Promise](https://github.com/reactphp/promise) that
will resolve with a [`Client`](#client)
instance on success or will reject with an `Exception` if the URL is
invalid or the connection or authentication fails.
The returned Promise is implemented in such a way that it can be
cancelled when it is still pending. Cancelling a pending promise will
reject its value with an Exception and will cancel the underlying TCP/IP
connection attempt and/or Redis authentication.
```php
$promise = $factory->createClient($uri);
Loop::addTimer(3.0, function () use ($promise) {
$promise->cancel();
});
```
The `$redisUri` can be given in the
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
`[redis[s]://][:auth@]host[:port][/db]`.
You can omit the URI scheme and port if you're connecting to the default port 6379:
```php
// both are equivalent due to defaults being applied
$factory->createClient('localhost');
$factory->createClient('redis://localhost:6379');
```
Redis supports password-based authentication (`AUTH` command). Note that Redis'
authentication mechanism does not employ a username, so you can pass the
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:
```php
// all forms are equivalent
$factory->createClient('redis://:h%40llo@localhost');
$factory->createClient('redis://ignored:h%40llo@localhost');
$factory->createClient('redis://localhost?password=h%40llo');
```
You can optionally include a path that will be used to select (SELECT command) the right database:
```php
// both forms are equivalent
$factory->createClient('redis://localhost/2');
$factory->createClient('redis://localhost?db=2');
```
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
```php
$factory->createClient('rediss://redis.example.com:6340');
```
You can use the `redis+unix://` URI scheme if your Redis instance is listening
on a Unix domain socket (UDS) path:
```php
$factory->createClient('redis+unix:///tmp/redis.sock');
// the URI MAY contain `password` and `db` query parameters as seen above
$factory->createClient('redis+unix:///tmp/redis.sock?password=secret&db=2');
// the URI MAY contain authentication details as userinfo as seen above
// should be used with care, also note that database can not be passed as path
$factory->createClient('redis+unix://:secret@/tmp/redis.sock');
```
This method respects PHP's `default_socket_timeout` setting (default 60s)
as a timeout for establishing the connection and waiting for successful
authentication. You can explicitly pass a custom timeout value in seconds
(or use a negative number to not apply a timeout) like this:
```php
$factory->createClient('localhost?timeout=0.5');
```
#### createLazyClient()
The `createLazyClient(string $uri): Client` method can be used to
create a new [`Client`](#client).
It helps with establishing a plain TCP/IP or secure TLS connection to Redis
and optionally authenticating (AUTH) and selecting the right database (SELECT).
```php
$redis = $factory->createLazyClient('localhost:6379');
$redis->incr('hello');
$redis->end();
```
This method immediately returns a "virtual" connection implementing the
[`Client`](#client) that can be used to interface with your Redis database.
Internally, it lazily creates the underlying database connection only on
demand once the first request is invoked on this instance and will queue
all outstanding requests until the underlying connection is ready.
Additionally, it will only keep this underlying connection in an "idle" state
for 60s by default and will automatically close the underlying connection when
it is no longer needed.
From a consumer side this means that you can start sending commands to the
database right away while the underlying connection may still be
outstanding. Because creating this underlying connection may take some
time, it will enqueue all oustanding commands and will ensure that all
commands will be executed in correct order once the connection is ready.
In other words, this "virtual" connection behaves just like a "real"
connection as described in the `Client` interface and frees you from having
to deal with its async resolution.
If the underlying database connection fails, it will reject all
outstanding commands and will return to the initial "idle" state. This
means that you can keep sending additional commands at a later time which
will again try to open a new underlying connection. Note that this may
require special care if you're using transactions (`MULTI`/`EXEC`) that are kept
open for longer than the idle period.
While using PubSub channels (see `SUBSCRIBE` and `PSUBSCRIBE` commands), this client
will never reach an "idle" state and will keep pending forever (or until the
underlying database connection is lost). Additionally, if the underlying
database connection drops, it will automatically send the appropriate `unsubscribe`
and `punsubscribe` events for all currently active channel and pattern subscriptions.
This allows you to react to these events and restore your subscriptions by
creating a new underlying connection repeating the above commands again.
Note that creating the underlying connection will be deferred until the
first request is invoked. Accordingly, any eventual connection issues
will be detected once this instance is first used. You can use the
`end()` method to ensure that the "virtual" connection will be soft-closed
and no further commands can be enqueued. Similarly, calling `end()` on
this instance when not currently connected will succeed immediately and
will not have to wait for an actual underlying connection.
Depending on your particular use case, you may prefer this method or the
underlying `createClient()` which resolves with a promise. For many
simple use cases it may be easier to create a lazy connection.
The `$redisUri` can be given in the
[standard](https://www.iana.org/assignments/uri-schemes/prov/redis) form
`[redis[s]://][:auth@]host[:port][/db]`.
You can omit the URI scheme and port if you're connecting to the default port 6379:
```php
// both are equivalent due to defaults being applied
$factory->createLazyClient('localhost');
$factory->createLazyClient('redis://localhost:6379');
```
Redis supports password-based authentication (`AUTH` command). Note that Redis'
authentication mechanism does not employ a username, so you can pass the
password `h@llo` URL-encoded (percent-encoded) as part of the URI like this:
```php
// all forms are equivalent
$factory->createLazyClient('redis://:h%40llo@localhost');
$factory->createLazyClient('redis://ignored:h%40llo@localhost');
$factory->createLazyClient('redis://localhost?password=h%40llo');
```
You can optionally include a path that will be used to select (SELECT command) the right database:
```php
// both forms are equivalent
$factory->createLazyClient('redis://localhost/2');
$factory->createLazyClient('redis://localhost?db=2');
```
You can use the [standard](https://www.iana.org/assignments/uri-schemes/prov/rediss)
`rediss://` URI scheme if you're using a secure TLS proxy in front of Redis:
```php
$factory->createLazyClient('rediss://redis.example.com:6340');
```
You can use the `redis+unix://` URI scheme if your Redis instance is listening
on a Unix domain socket (UDS) path:
```php
$factory->createLazyClient('redis+unix:///tmp/redis.sock');
// the URI MAY contain `password` and `db` query parameters as seen above
$factory->createLazyClient('redis+unix:///tmp/redis.sock?password=secret&db=2');
// the URI MAY contain authentication details as userinfo as seen above
// should be used with care, also note that database can not be passed as path
$factory->createLazyClient('redis+unix://:secret@/tmp/redis.sock');
```
This method respects PHP's `default_socket_timeout` setting (default 60s)
as a timeout for establishing the underlying connection and waiting for
successful authentication. You can explicitly pass a custom timeout value
in seconds (or use a negative number to not apply a timeout) like this:
```php
$factory->createLazyClient('localhost?timeout=0.5');
```
By default, this method will keep "idle" connections open for 60s and will
then end the underlying connection. The next request after an "idle"
connection ended will automatically create a new underlying connection.
This ensure you always get a "fresh" connection and as such should not be
confused with a "keepalive" or "heartbeat" mechanism, as this will not
actively try to probe the connection. You can explicitly pass a custom
idle timeout value in seconds (or use a negative number to not apply a
timeout) like this:
```php
$factory->createLazyClient('localhost?idle=0.1');
```
### Client
The `Client` is responsible for exchanging messages with Redis
and keeps track of pending commands.
Besides defining a few methods, this interface also implements the
`EventEmitterInterface` which allows you to react to certain events as documented below.
#### __call()
The `__call(string $name, string[] $args): PromiseInterface<mixed,Exception>` method can be used to
invoke the given command.
This is a magic method that will be invoked when calling any Redis command on this instance.
Each method call matches the respective [Redis command](https://redis.io/commands).
For example, the `$redis->get()` method will invoke the [`GET` command](https://redis.io/commands/get).
```php
$redis->get($key)->then(function (?string $value) {
var_dump($value);
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
All [Redis commands](https://redis.io/commands) are automatically available as
public methods via this magic `__call()` method.
Listing all available commands is out of scope here, please refer to the
[Redis command reference](https://redis.io/commands).
Any arguments passed to the method call will be forwarded as command arguments.
For example, the `$redis->set('name', 'Alice')` call will perform the equivalent of a
`SET name Alice` command. It's safe to pass integer arguments where applicable (for
example `$redis->expire($key, 60)`), but internally Redis requires all arguments to
always be coerced to string values.
Each of these commands supports async operation and returns a [Promise](#promises)
that eventually *fulfills* with its *results* on success or *rejects* with an
`Exception` on error. See also [promises](#promises) for more details.
#### end()
The `end():void` method can be used to
soft-close the Redis connection once all pending commands are completed.
#### close()
The `close():void` method can be used to
force-close the Redis connection and reject all pending commands.
#### error event
The `error` event will be emitted once a fatal error occurs, such as
when the client connection is lost or is invalid.
The event receives a single `Exception` argument for the error instance.
```php
$redis->on('error', function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```
This event will only be triggered for fatal errors and will be followed
by closing the client connection. It is not to be confused with "soft"
errors caused by invalid commands.
#### close event
The `close` event will be emitted once the client connection closes (terminates).
```php
$redis->on('close', function () {
echo 'Connection closed' . PHP_EOL;
});
```
See also the [`close()`](#close) method.
## 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 clue/redis-react:^2.6
```
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 contains both unit tests and functional integration tests.
The functional tests require access to a running Redis server instance
and will be skipped by default.
If you don't have access to a running Redis server, you can also use a temporary `Redis` Docker image:
```bash
$ docker run --net=host redis
```
To now run the functional tests, you need to supply *your* login
details in an environment variable like this:
```bash
$ REDIS_URI=localhost:6379 vendor/bin/phpunit
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

32
vendor/clue/redis-react/composer.json vendored Normal file
View File

@ -0,0 +1,32 @@
{
"name": "clue/redis-react",
"description": "Async Redis client implementation, built on top of ReactPHP.",
"keywords": ["Redis", "database", "client", "async", "ReactPHP"],
"homepage": "https://github.com/clue/reactphp-redis",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"require": {
"php": ">=5.3",
"clue/redis-protocol": "0.3.*",
"evenement/evenement": "^3.0 || ^2.0 || ^1.0",
"react/event-loop": "^1.2",
"react/promise": "^2.0 || ^1.1",
"react/promise-timer": "^1.8",
"react/socket": "^1.9"
},
"require-dev": {
"clue/block-react": "^1.1",
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
},
"autoload": {
"psr-4": { "Clue\\React\\Redis\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Clue\\Tests\\React\\Redis\\": "tests/" }
}
}

54
vendor/clue/redis-react/src/Client.php vendored Normal file
View File

@ -0,0 +1,54 @@
<?php
namespace Clue\React\Redis;
use Evenement\EventEmitterInterface;
use React\Promise\PromiseInterface;
/**
* Simple interface for executing redis commands
*
* @event error(Exception $error)
* @event close()
*
* @event message($channel, $message)
* @event subscribe($channel, $numberOfChannels)
* @event unsubscribe($channel, $numberOfChannels)
*
* @event pmessage($pattern, $channel, $message)
* @event psubscribe($channel, $numberOfChannels)
* @event punsubscribe($channel, $numberOfChannels)
*/
interface Client extends EventEmitterInterface
{
/**
* Invoke the given command and return a Promise that will be fulfilled when the request has been replied to
*
* This is a magic method that will be invoked when calling any redis
* command on this instance.
*
* @param string $name
* @param string[] $args
* @return PromiseInterface Promise<mixed,Exception>
*/
public function __call($name, $args);
/**
* end connection once all pending requests have been replied to
*
* @return void
* @uses self::close() once all replies have been received
* @see self::close() for closing the connection immediately
*/
public function end();
/**
* close connection immediately
*
* This will emit the "close" event.
*
* @return void
* @see self::end() for closing the connection once the client is idle
*/
public function close();
}

191
vendor/clue/redis-react/src/Factory.php vendored Normal file
View File

@ -0,0 +1,191 @@
<?php
namespace Clue\React\Redis;
use Clue\Redis\Protocol\Factory as ProtocolFactory;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise\Timer\TimeoutException;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
class Factory
{
/** @var LoopInterface */
private $loop;
/** @var ConnectorInterface */
private $connector;
/** @var ProtocolFactory */
private $protocol;
/**
* @param ?LoopInterface $loop
* @param ?ConnectorInterface $connector
* @param ?ProtocolFactory $protocol
*/
public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, ProtocolFactory $protocol = null)
{
$this->loop = $loop ?: Loop::get();
$this->connector = $connector ?: new Connector(array(), $this->loop);
$this->protocol = $protocol ?: new ProtocolFactory();
}
/**
* Create Redis client connected to address of given redis instance
*
* @param string $uri Redis server URI to connect to
* @return \React\Promise\PromiseInterface<Client,\Exception> Promise that will
* be fulfilled with `Client` on success or rejects with `\Exception` on error.
*/
public function createClient($uri)
{
// support `redis+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^(redis\+unix:\/\/(?:[^:]*:[^@]*@)?)(.+?)?$/', $uri, $match)) {
$parts = parse_url($match[1] . 'localhost/' . $match[2]);
} else {
if (strpos($uri, '://') === false) {
$uri = 'redis://' . $uri;
}
$parts = parse_url($uri);
}
$uri = preg_replace(array('/(:)[^:\/]*(@)/', '/([?&]password=).*?($|&)/'), '$1***$2', $uri);
if ($parts === false || !isset($parts['scheme'], $parts['host']) || !in_array($parts['scheme'], array('redis', 'rediss', 'redis+unix'))) {
return \React\Promise\reject(new \InvalidArgumentException(
'Invalid Redis URI given (EINVAL)',
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
));
}
$args = array();
parse_str(isset($parts['query']) ? $parts['query'] : '', $args);
$authority = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 6379);
if ($parts['scheme'] === 'rediss') {
$authority = 'tls://' . $authority;
} elseif ($parts['scheme'] === 'redis+unix') {
$authority = 'unix://' . substr($parts['path'], 1);
unset($parts['path']);
}
$connecting = $this->connector->connect($authority);
$deferred = new Deferred(function ($_, $reject) use ($connecting, $uri) {
// connection cancelled, start with rejecting attempt, then clean up
$reject(new \RuntimeException(
'Connection to ' . $uri . ' cancelled (ECONNABORTED)',
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
));
// either close successful connection or cancel pending connection attempt
$connecting->then(function (ConnectionInterface $connection) {
$connection->close();
});
$connecting->cancel();
});
$protocol = $this->protocol;
$promise = $connecting->then(function (ConnectionInterface $stream) use ($protocol) {
return new StreamingClient($stream, $protocol->createResponseParser(), $protocol->createSerializer());
}, function (\Exception $e) use ($uri) {
throw new \RuntimeException(
'Connection to ' . $uri . ' failed: ' . $e->getMessage(),
$e->getCode(),
$e
);
});
// use `?password=secret` query or `user:secret@host` password form URL
$pass = isset($args['password']) ? $args['password'] : (isset($parts['pass']) ? rawurldecode($parts['pass']) : null);
if (isset($args['password']) || isset($parts['pass'])) {
$pass = isset($args['password']) ? $args['password'] : rawurldecode($parts['pass']);
$promise = $promise->then(function (StreamingClient $redis) use ($pass, $uri) {
return $redis->auth($pass)->then(
function () use ($redis) {
return $redis;
},
function (\Exception $e) use ($redis, $uri) {
$redis->close();
$const = '';
$errno = $e->getCode();
if ($errno === 0) {
$const = ' (EACCES)';
$errno = $e->getCode() ?: (defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
}
throw new \RuntimeException(
'Connection to ' . $uri . ' failed during AUTH command: ' . $e->getMessage() . $const,
$errno,
$e
);
}
);
});
}
// use `?db=1` query or `/1` path (skip first slash)
if (isset($args['db']) || (isset($parts['path']) && $parts['path'] !== '/')) {
$db = isset($args['db']) ? $args['db'] : substr($parts['path'], 1);
$promise = $promise->then(function (StreamingClient $redis) use ($db, $uri) {
return $redis->select($db)->then(
function () use ($redis) {
return $redis;
},
function (\Exception $e) use ($redis, $uri) {
$redis->close();
$const = '';
$errno = $e->getCode();
if ($errno === 0 && strpos($e->getMessage(), 'NOAUTH ') === 0) {
$const = ' (EACCES)';
$errno = defined('SOCKET_EACCES') ? SOCKET_EACCES : 13;
} elseif ($errno === 0) {
$const = ' (ENOENT)';
$errno = defined('SOCKET_ENOENT') ? SOCKET_ENOENT : 2;
}
throw new \RuntimeException(
'Connection to ' . $uri . ' failed during SELECT command: ' . $e->getMessage() . $const,
$errno,
$e
);
}
);
});
}
$promise->then(array($deferred, 'resolve'), array($deferred, 'reject'));
// use timeout from explicit ?timeout=x parameter or default to PHP's default_socket_timeout (60)
$timeout = isset($args['timeout']) ? (float) $args['timeout'] : (int) ini_get("default_socket_timeout");
if ($timeout < 0) {
return $deferred->promise();
}
return \React\Promise\Timer\timeout($deferred->promise(), $timeout, $this->loop)->then(null, function ($e) use ($uri) {
if ($e instanceof TimeoutException) {
throw new \RuntimeException(
'Connection to ' . $uri . ' timed out after ' . $e->getTimeout() . ' seconds (ETIMEDOUT)',
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
);
}
throw $e;
});
}
/**
* Create Redis client connected to address of given redis instance
*
* @param string $target
* @return Client
*/
public function createLazyClient($target)
{
return new LazyClient($target, $this, $this->loop);
}
}

View File

@ -0,0 +1,219 @@
<?php
namespace Clue\React\Redis;
use Evenement\EventEmitter;
use React\Stream\Util;
use React\EventLoop\LoopInterface;
/**
* @internal
*/
class LazyClient extends EventEmitter implements Client
{
private $target;
/** @var Factory */
private $factory;
private $closed = false;
private $promise;
private $loop;
private $idlePeriod = 60.0;
private $idleTimer;
private $pending = 0;
private $subscribed = array();
private $psubscribed = array();
/**
* @param $target
*/
public function __construct($target, Factory $factory, LoopInterface $loop)
{
$args = array();
\parse_str((string) \parse_url($target, \PHP_URL_QUERY), $args);
if (isset($args['idle'])) {
$this->idlePeriod = (float)$args['idle'];
}
$this->target = $target;
$this->factory = $factory;
$this->loop = $loop;
}
private function client()
{
if ($this->promise !== null) {
return $this->promise;
}
$self = $this;
$pending =& $this->promise;
$idleTimer=& $this->idleTimer;
$subscribed =& $this->subscribed;
$psubscribed =& $this->psubscribed;
$loop = $this->loop;
return $pending = $this->factory->createClient($this->target)->then(function (Client $redis) use ($self, &$pending, &$idleTimer, &$subscribed, &$psubscribed, $loop) {
// connection completed => remember only until closed
$redis->on('close', function () use (&$pending, $self, &$subscribed, &$psubscribed, &$idleTimer, $loop) {
$pending = null;
// foward unsubscribe/punsubscribe events when underlying connection closes
$n = count($subscribed);
foreach ($subscribed as $channel => $_) {
$self->emit('unsubscribe', array($channel, --$n));
}
$n = count($psubscribed);
foreach ($psubscribed as $pattern => $_) {
$self->emit('punsubscribe', array($pattern, --$n));
}
$subscribed = array();
$psubscribed = array();
if ($idleTimer !== null) {
$loop->cancelTimer($idleTimer);
$idleTimer = null;
}
});
// keep track of all channels and patterns this connection is subscribed to
$redis->on('subscribe', function ($channel) use (&$subscribed) {
$subscribed[$channel] = true;
});
$redis->on('psubscribe', function ($pattern) use (&$psubscribed) {
$psubscribed[$pattern] = true;
});
$redis->on('unsubscribe', function ($channel) use (&$subscribed) {
unset($subscribed[$channel]);
});
$redis->on('punsubscribe', function ($pattern) use (&$psubscribed) {
unset($psubscribed[$pattern]);
});
Util::forwardEvents(
$redis,
$self,
array(
'message',
'subscribe',
'unsubscribe',
'pmessage',
'psubscribe',
'punsubscribe',
)
);
return $redis;
}, function (\Exception $e) use (&$pending) {
// connection failed => discard connection attempt
$pending = null;
throw $e;
});
}
public function __call($name, $args)
{
if ($this->closed) {
return \React\Promise\reject(new \RuntimeException(
'Connection closed (ENOTCONN)',
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
));
}
$that = $this;
return $this->client()->then(function (Client $redis) use ($name, $args, $that) {
$that->awake();
return \call_user_func_array(array($redis, $name), $args)->then(
function ($result) use ($that) {
$that->idle();
return $result;
},
function ($error) use ($that) {
$that->idle();
throw $error;
}
);
});
}
public function end()
{
if ($this->promise === null) {
$this->close();
}
if ($this->closed) {
return;
}
$that = $this;
return $this->client()->then(function (Client $redis) use ($that) {
$redis->on('close', function () use ($that) {
$that->close();
});
$redis->end();
});
}
public function close()
{
if ($this->closed) {
return;
}
$this->closed = true;
// either close active connection or cancel pending connection attempt
if ($this->promise !== null) {
$this->promise->then(function (Client $redis) {
$redis->close();
});
if ($this->promise !== null) {
$this->promise->cancel();
$this->promise = null;
}
}
if ($this->idleTimer !== null) {
$this->loop->cancelTimer($this->idleTimer);
$this->idleTimer = null;
}
$this->emit('close');
$this->removeAllListeners();
}
/**
* @internal
*/
public function awake()
{
++$this->pending;
if ($this->idleTimer !== null) {
$this->loop->cancelTimer($this->idleTimer);
$this->idleTimer = null;
}
}
/**
* @internal
*/
public function idle()
{
--$this->pending;
if ($this->pending < 1 && $this->idlePeriod >= 0 && !$this->subscribed && !$this->psubscribed && $this->promise !== null) {
$idleTimer =& $this->idleTimer;
$promise =& $this->promise;
$idleTimer = $this->loop->addTimer($this->idlePeriod, function () use (&$idleTimer, &$promise) {
$promise->then(function (Client $redis) {
$redis->close();
});
$promise = null;
$idleTimer = null;
});
}
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace Clue\React\Redis;
use Clue\Redis\Protocol\Factory as ProtocolFactory;
use Clue\Redis\Protocol\Model\ErrorReply;
use Clue\Redis\Protocol\Model\ModelInterface;
use Clue\Redis\Protocol\Model\MultiBulkReply;
use Clue\Redis\Protocol\Parser\ParserException;
use Clue\Redis\Protocol\Parser\ParserInterface;
use Clue\Redis\Protocol\Serializer\SerializerInterface;
use Evenement\EventEmitter;
use React\Promise\Deferred;
use React\Stream\DuplexStreamInterface;
/**
* @internal
*/
class StreamingClient extends EventEmitter implements Client
{
private $stream;
private $parser;
private $serializer;
private $requests = array();
private $ending = false;
private $closed = false;
private $subscribed = 0;
private $psubscribed = 0;
public function __construct(DuplexStreamInterface $stream, ParserInterface $parser = null, SerializerInterface $serializer = null)
{
if ($parser === null || $serializer === null) {
$factory = new ProtocolFactory();
if ($parser === null) {
$parser = $factory->createResponseParser();
}
if ($serializer === null) {
$serializer = $factory->createSerializer();
}
}
$that = $this;
$stream->on('data', function($chunk) use ($parser, $that) {
try {
$models = $parser->pushIncoming($chunk);
} catch (ParserException $error) {
$that->emit('error', array(new \UnexpectedValueException(
'Invalid data received: ' . $error->getMessage() . ' (EBADMSG)',
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG : 77,
$error
)));
$that->close();
return;
}
foreach ($models as $data) {
try {
$that->handleMessage($data);
} catch (\UnderflowException $error) {
$that->emit('error', array($error));
$that->close();
return;
}
}
});
$stream->on('close', array($this, 'close'));
$this->stream = $stream;
$this->parser = $parser;
$this->serializer = $serializer;
}
public function __call($name, $args)
{
$request = new Deferred();
$promise = $request->promise();
$name = strtolower($name);
// special (p)(un)subscribe commands only accept a single parameter and have custom response logic applied
static $pubsubs = array('subscribe', 'unsubscribe', 'psubscribe', 'punsubscribe');
if ($this->ending) {
$request->reject(new \RuntimeException(
'Connection ' . ($this->closed ? 'closed' : 'closing'). ' (ENOTCONN)',
defined('SOCKET_ENOTCONN') ? SOCKET_ENOTCONN : 107
));
} elseif (count($args) !== 1 && in_array($name, $pubsubs)) {
$request->reject(new \InvalidArgumentException(
'PubSub commands limited to single argument (EINVAL)',
defined('SOCKET_EINVAL') ? SOCKET_EINVAL : 22
));
} elseif ($name === 'monitor') {
$request->reject(new \BadMethodCallException(
'MONITOR command explicitly not supported (ENOTSUP)',
defined('SOCKET_ENOTSUP') ? SOCKET_ENOTSUP : (defined('SOCKET_EOPNOTSUPP') ? SOCKET_EOPNOTSUPP : 95)
));
} else {
$this->stream->write($this->serializer->getRequestMessage($name, $args));
$this->requests []= $request;
}
if (in_array($name, $pubsubs)) {
$that = $this;
$subscribed =& $this->subscribed;
$psubscribed =& $this->psubscribed;
$promise->then(function ($array) use ($that, &$subscribed, &$psubscribed) {
$first = array_shift($array);
// (p)(un)subscribe messages are to be forwarded
$that->emit($first, $array);
// remember number of (p)subscribe topics
if ($first === 'subscribe' || $first === 'unsubscribe') {
$subscribed = $array[1];
} else {
$psubscribed = $array[1];
}
});
}
return $promise;
}
public function handleMessage(ModelInterface $message)
{
if (($this->subscribed !== 0 || $this->psubscribed !== 0) && $message instanceof MultiBulkReply) {
$array = $message->getValueNative();
$first = array_shift($array);
// pub/sub messages are to be forwarded and should not be processed as request responses
if (in_array($first, array('message', 'pmessage'))) {
$this->emit($first, $array);
return;
}
}
if (!$this->requests) {
throw new \UnderflowException(
'Unexpected reply received, no matching request found (ENOMSG)',
defined('SOCKET_ENOMSG') ? SOCKET_ENOMSG : 42
);
}
$request = array_shift($this->requests);
assert($request instanceof Deferred);
if ($message instanceof ErrorReply) {
$request->reject($message);
} else {
$request->resolve($message->getValueNative());
}
if ($this->ending && !$this->requests) {
$this->close();
}
}
public function end()
{
$this->ending = true;
if (!$this->requests) {
$this->close();
}
}
public function close()
{
if ($this->closed) {
return;
}
$this->ending = true;
$this->closed = true;
$remoteClosed = $this->stream->isReadable() === false && $this->stream->isWritable() === false;
$this->stream->close();
$this->emit('close');
// reject all remaining requests in the queue
while ($this->requests) {
$request = array_shift($this->requests);
assert($request instanceof Deferred);
if ($remoteClosed) {
$request->reject(new \RuntimeException(
'Connection closed by peer (ECONNRESET)',
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104
));
} else {
$request->reject(new \RuntimeException(
'Connection closing (ECONNABORTED)',
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
));
}
}
}
}

29
vendor/clue/soap-react/.travis.yml vendored Normal file
View File

@ -0,0 +1,29 @@
language: php
php:
# - 5.3 # requires old distro, see below
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm # ignore errors, see below
# lock distro so new future defaults will not break the build
dist: trusty
matrix:
include:
- php: 5.3
dist: precise
allow_failures:
- php: hhvm
sudo: false
install:
- composer install --no-interaction
script:
- vendor/bin/phpunit --coverage-text

115
vendor/clue/soap-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,115 @@
# Changelog
## 1.0.0 (2018-11-07)
* First stable release, now following SemVer!
I'd like to thank [Bergfreunde GmbH](https://www.bergfreunde.de/), a German-based
online retailer for Outdoor Gear & Clothing, for sponsoring large parts of this development! 🎉
Thanks to sponsors like this, who understand the importance of open source
development, I can justify spending time and focus on open source development
instead of traditional paid work.
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.
* BC break / Feature: Replace `Factory` with simplified `Client` constructor,
add support for optional SOAP options and non-WSDL mode and
respect WSDL type definitions when decoding and support classmap option.
(#31, #32 and #33 by @clue)
```php
// old
$factory = new Factory($loop);
$client = $factory->createClientFromWsdl($wsdl);
// new
$browser = new Browser($loop);
$client = new Client($browser, $wsdl);
```
The `Client` constructor now accepts an array of options. All given options will
be passed through to the underlying `SoapClient`. However, not all options
make sense in this async implementation and as such may not have the desired
effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php)
documentation for more details.
If working in WSDL mode, the `$options` parameter is optional. If working in
non-WSDL mode, the WSDL parameter must be set to `null` and the options
parameter must contain the `location` and `uri` options, where `location` is
the URL of the SOAP server to send the request to, and `uri` is the target
namespace of the SOAP service:
```php
$client = new Client($browser, null, array(
'location' => 'http://example.com',
'uri' => 'http://ping.example.com',
));
```
* BC break: Mark all classes as final and all internal APIs as `@internal`.
(#26 and #37 by @clue)
* Feature: Add new `Client::withLocation()` method.
(#38 by @floriansimon1, @pascal-hofmann and @clue)
The `withLocation(string $location): self` method can be used to
return a new `Client` with the updated location (URI) for all functions.
Note that this is not to be confused with the WSDL file location.
A WSDL file can contain any number of function definitions.
It's very common that all of these functions use the same location definition.
However, technically each function can potentially use a different location.
```php
$client = $client->withLocation('http://example.com/soap');
assert('http://example.com/soap' === $client->getLocation('echo'));
```
As an alternative to this method, you can also set the `location` option
in the `Client` constructor (such as when in non-WSDL mode).
* Feature: Properly handle SOAP error responses, accept HTTP error responses and do not follow any HTTP redirects.
(#35 by @clue)
* Improve documentation and update project homepage,
documentation for HTTP proxy servers,
support timeouts for SOAP requests (HTTP timeout option) and
add cancellation support.
(#25, #29, #30 #34 and #36 by @clue)
* Improve test suite by supporting PHPUnit 6,
optionally skip functional integration tests requiring internet and
test against PHP 7.2 and PHP 7.1 and latest ReactPHP components.
(#24 by @carusogabriel and #27 and #28 by @clue)
## 0.2.0 (2017-10-02)
* Feature: Added the possibility to use local WSDL files
(#11 by @floriansimon1)
```php
$factory = new Factory($loop);
$wsdl = file_get_contents('service.wsdl');
$client = $factory->createClientFromWsdl($wsdl);
```
* Feature: Add `Client::getLocation()` helper
(#13 by @clue)
* Feature: Forward compatibility with clue/buzz-react v2.0 and upcoming EventLoop
(#9 by @floriansimon1 and #19 and #21 by @clue)
* Improve test suite by adding PHPUnit to require-dev and
test PHP 5.3 through PHP 7.0 and HHVM and
fix Travis build config
(#1 by @WyriHaximus and #12, #17 and #22 by @clue)
## 0.1.0 (2014-07-28)
* First tagged release
## 0.0.0 (2014-07-20)
* Initial concept

21
vendor/clue/soap-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Christian Lück
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.

442
vendor/clue/soap-react/README.md vendored Normal file
View File

@ -0,0 +1,442 @@
# clue/reactphp-soap [![Build Status](https://travis-ci.org/clue/reactphp-soap.svg?branch=master)](https://travis-ci.org/clue/reactphp-soap)
Simple, async [SOAP](https://en.wikipedia.org/wiki/SOAP) web service client library,
built on top of [ReactPHP](https://reactphp.org/).
Most notably, SOAP is often used for invoking
[Remote procedure calls](https://en.wikipedia.org/wiki/Remote_procedure_call) (RPCs)
in distributed systems.
Internally, SOAP messages are encoded as XML and usually sent via HTTP POST requests.
For the most part, SOAP (originally *Simple Object Access protocol*) is a protocol of the past,
and in fact anything but *simple*.
It is still in use by many (often *legacy*) systems.
This project provides a *simple* API for invoking *async* RPCs to remote web services.
* **Async execution of functions** -
Send any number of functions (RPCs) to the remote web service in parallel and
process their responses as soon as results come in.
The Promise-based design provides a *sane* interface to working with out of bound responses.
* **Async processing of the WSDL** -
The WSDL (web service description language) file will be downloaded and processed
in the background.
* **Event-driven core** -
Internally, everything uses event handlers to react to incoming events, such as an incoming RPC result.
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
Built on top of tested components instead of re-inventing the wheel.
* **Good test coverage** -
Comes with an automated tests suite and is regularly tested against actual web services in the wild.
**Table of contents**
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [Client](#client)
* [soapCall()](#soapcall)
* [getFunctions()](#getfunctions)
* [getTypes()](#gettypes)
* [getLocation()](#getlocation)
* [withLocation()](#withlocation)
* [Proxy](#proxy)
* [Functions](#functions)
* [Promises](#promises)
* [Cancellation](#cancellation)
* [Timeouts](#timeouts)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Quickstart example
Once [installed](#install), you can use the following code to query an example
web service via SOAP:
```php
$loop = React\EventLoop\Factory::create();
$browser = new Browser($loop);
$wsdl = 'http://example.com/demo.wsdl';
$browser->get($wsdl)->then(function (ResponseInterface $response) use ($browser) {
$client = new Client($browser, (string)$response->getBody());
$api = new Proxy($client);
$api->getBank(array('blz' => '12070000'))->then(function ($result) {
var_dump('Result', $result);
});
});
$loop->run();
```
See also the [examples](examples).
## Usage
### Client
The `Client` class is responsible for communication with the remote SOAP
WebService server.
It requires a [`Browser`](https://github.com/clue/reactphp-buzz#browser) object
bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage)
in order to handle async requests, the WSDL file contents and an optional
array of SOAP options:
```php
$loop = React\EventLoop\Factory::create();
$browser = new Clue\React\Buzz\Browser($loop);
$wsdl = '<?xml …';
$options = array();
$client = new Client($browser, $wsdl, $options);
```
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)
to the [`Browser`](https://github.com/clue/reactphp-buzz#browser) instance:
```php
$connector = new \React\Socket\Connector($loop, 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 Browser($loop, $connector);
$client = new Client($browser, $wsdl);
```
The `Client` works similar to PHP's `SoapClient` (which it uses under the
hood), but leaves you the responsibility to load the WSDL file. This allows
you to use local WSDL files, WSDL files from a cache or the most common form,
downloading the WSDL file contents from an URL through the `Browser`:
```php
$browser = new Browser($loop);
$browser->get($url)->then(
function (ResponseInterface $response) use ($browser) {
// WSDL file is ready, create client
$client = new Client($browser, (string)$response->getBody());
// do something…
},
function (Exception $e) {
// an error occured while trying to download the WSDL
}
);
```
The `Client` constructor loads the given WSDL file contents into memory and
parses its definition. If the given WSDL file is invalid and can not be
parsed, this will throw a `SoapFault`:
```php
try {
$client = new Client($browser, $wsdl);
} catch (SoapFault $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
```
> Note that if you have `ext-xdebug` loaded, this may halt with a fatal
error instead of throwing a `SoapFault`. It is not recommended to use this
extension in production, so this should only ever affect test environments.
The `Client` constructor accepts an array of options. All given options will
be passed through to the underlying `SoapClient`. However, not all options
make sense in this async implementation and as such may not have the desired
effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php)
documentation for more details.
If working in WSDL mode, the `$options` parameter is optional. If working in
non-WSDL mode, the WSDL parameter must be set to `null` and the options
parameter must contain the `location` and `uri` options, where `location` is
the URL of the SOAP server to send the request to, and `uri` is the target
namespace of the SOAP service:
```php
$client = new Client($browser, null, array(
'location' => 'http://example.com',
'uri' => 'http://ping.example.com',
));
```
Similarly, if working in WSDL mode, the `location` option can be used to
explicitly overwrite the URL of the SOAP server to send the request to:
```php
$client = new Client($browser, $wsdl, array(
'location' => 'http://example.com'
));
```
You can use the `soap_version` option to change from the default SOAP 1.1 to
use SOAP 1.2 instead:
```php
$client = new Client($browser, $wsdl, array(
'soap_version' => SOAP_1_2
));
```
You can use the `classmap` option to map certain WSDL types to PHP classes
like this:
```php
$client = new Client($browser, $wsdl, array(
'classmap' => array(
'getBankResponseType' => BankResponse::class
)
));
```
The `proxy_host` option (and family) is not supported by this library. As an
alternative, you can configure the given `$browser` instance to use an
[HTTP proxy server](https://github.com/clue/reactphp-buzz#http-proxy).
If you find any other option is missing or not supported here, PRs are much
appreciated!
All public methods of the `Client` are considered *advanced usage*.
If you want to call RPC functions, see below for the [`Proxy`](#proxy) class.
#### soapCall()
The `soapCall(string $method, mixed[] $arguments): PromiseInterface<mixed, Exception>` method can be used to
queue the given function to be sent via SOAP and wait for a response from the remote web service.
```php
// advanced usage, see Proxy for recommended alternative
$promise = $client->soapCall('ping', array('hello', 42));
```
Note: This is considered *advanced usage*, you may want to look into using the [`Proxy`](#proxy) instead.
```php
$proxy = new Proxy($client);
$promise = $proxy->ping('hello', 42);
```
#### getFunctions()
The `getFunctions(): string[]|null` method can be used to
return an array of functions defined in the WSDL.
It returns the equivalent of PHP's
[`SoapClient::__getFunctions()`](http://php.net/manual/en/soapclient.getfunctions.php).
In non-WSDL mode, this method returns `null`.
#### getTypes()
The `getTypes(): string[]|null` method can be used to
return an array of types defined in the WSDL.
It returns the equivalent of PHP's
[`SoapClient::__getTypes()`](http://php.net/manual/en/soapclient.gettypes.php).
In non-WSDL mode, this method returns `null`.
#### getLocation()
The `getLocation(string|int $function): string` method can be used to
return the location (URI) of the given webservice `$function`.
Note that this is not to be confused with the WSDL file location.
A WSDL file can contain any number of function definitions.
It's very common that all of these functions use the same location definition.
However, technically each function can potentially use a different location.
The `$function` parameter should be a string with the the SOAP function name.
See also [`getFunctions()`](#getfunctions) for a list of all available functions.
```php
assert('http://example.com/soap/service' === $client->getLocation('echo'));
```
For easier access, this function also accepts a numeric function index.
It then uses [`getFunctions()`](#getfunctions) internally to get the function
name for the given index.
This is particularly useful for the very common case where all functions use the
same location and accessing the first location is sufficient.
```php
assert('http://example.com/soap/service' === $client->getLocation(0));
```
When the `location` option has been set in the `Client` constructor
(such as when in non-WSDL mode) or via the `withLocation()` method, this
method returns the value of the given location.
Passing a `$function` not defined in the WSDL file will throw a `SoapFault`.
#### withLocation()
The `withLocation(string $location): self` method can be used to
return a new `Client` with the updated location (URI) for all functions.
Note that this is not to be confused with the WSDL file location.
A WSDL file can contain any number of function definitions.
It's very common that all of these functions use the same location definition.
However, technically each function can potentially use a different location.
```php
$client = $client->withLocation('http://example.com/soap');
assert('http://example.com/soap' === $client->getLocation('echo'));
```
As an alternative to this method, you can also set the `location` option
in the `Client` constructor (such as when in non-WSDL mode).
### Proxy
The `Proxy` class wraps an existing [`Client`](#client) instance in order to ease calling
SOAP functions.
```php
$proxy = new Proxy($client);
```
> Note that this class is called "Proxy" because it will forward (proxy) all
method calls to the actual SOAP service via the underlying
[`Client::soapCall()`](#soapcall) method. This is not to be confused with
using a proxy server. See [`Client`](#client) documentation for more
details on how to use an HTTP proxy server.
#### Functions
Each and every method call to the `Proxy` class will be sent via SOAP.
```php
$proxy->myMethod($myArg1, $myArg2)->then(function ($response) {
// result received
});
```
Please refer to your WSDL or its accompanying documentation for details
on which functions and arguments are supported.
#### Promises
Issuing SOAP functions is async (non-blocking), so you can actually send multiple RPC requests in parallel.
The web service will respond to each request with a return value. The order is not guaranteed.
Sending requests uses a [Promise](https://github.com/reactphp/promise)-based interface that makes it easy to react to when a request is *fulfilled*
(i.e. either successfully resolved or rejected with an error):
```php
$proxy->demo()->then(
function ($response) {
// response received for demo function
},
function (Exception $e) {
// an error occured while executing the request
}
});
```
#### Cancellation
The returned Promise is implemented in such a way that it can be cancelled
when it is still pending.
Cancelling a pending promise will reject its value with an Exception and
clean up any underlying resources.
```php
$promise = $proxy->demo();
$loop->addTimer(2.0, function () use ($promise) {
$promise->cancel();
});
```
#### Timeouts
This library uses a very efficient HTTP implementation, so most SOAP requests
should usually be completed in mere milliseconds. However, when sending SOAP
requests over an unreliable network (the internet), there are a number of things
that can go wrong and may cause the request to fail after a time. As such,
timeouts are handled by the underlying HTTP library and this library respects
PHP's `default_socket_timeout` setting (default 60s) as a timeout for sending the
outgoing SOAP request and waiting for a successful response and will otherwise
cancel the pending request and reject its value with an Exception.
Note that this timeout value covers creating the underlying transport connection,
sending the SOAP request, waiting for the remote service to process the request
and receiving the full SOAP response. To pass a custom timeout value, you can
assign the underlying [`timeout` option](https://github.com/clue/reactphp-buzz#timeouts)
like this:
```php
$browser = new Browser($loop);
$browser = $browser->withOptions(array(
'timeout' => 10.0
));
$client = new Client($browser, $wsdl);
$proxy = new Proxy($client);
$proxy->demo()->then(function ($response) {
// response received within 10 seconds maximum
var_dump($response);
});
```
Similarly, you can use a negative timeout value to not apply a timeout at all
or use a `null` value to restore the default handling. Note that the underlying
connection may still impose a different timeout value. See also the underlying
[`timeout` option](https://github.com/clue/reactphp-buzz#timeouts) for more details.
## 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 clue/soap-react:^1.0
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus only requires `ext-soap` and
supports running on legacy PHP 5.3 through current PHP 7+ 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
$ php 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
$ php vendor/bin/phpunit --exclude-group internet
```
## License
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

27
vendor/clue/soap-react/composer.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
"name": "clue/soap-react",
"description": "Simple, async SOAP webservice client library, built on top of ReactPHP",
"keywords": ["SOAP", "SoapClient", "WebService", "WSDL", "ReactPHP"],
"homepage": "https://github.com/clue/reactphp-soap",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@lueck.tv"
}
],
"autoload": {
"psr-4": { "Clue\\React\\Soap\\": "src/" }
},
"require": {
"php": ">=5.3",
"clue/buzz-react": "^2.5",
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"react/promise": "^2.1 || ^1.2",
"ext-soap": "*"
},
"require-dev": {
"clue/block-react": "^1.0",
"phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"
}
}

19
vendor/clue/soap-react/phpunit.xml.dist vendored Normal file
View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite>
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./src/</directory>
</whitelist>
</filter>
</phpunit>

323
vendor/clue/soap-react/src/Client.php vendored Normal file
View File

@ -0,0 +1,323 @@
<?php
namespace Clue\React\Soap;
use Clue\React\Buzz\Browser;
use Clue\React\Soap\Protocol\ClientDecoder;
use Clue\React\Soap\Protocol\ClientEncoder;
use Psr\Http\Message\ResponseInterface;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
/**
* The `Client` class is responsible for communication with the remote SOAP
* WebService server.
*
* It requires a [`Browser`](https://github.com/clue/reactphp-buzz#browser) object
* bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage)
* in order to handle async requests, the WSDL file contents and an optional
* array of SOAP options:
*
* ```php
* $loop = React\EventLoop\Factory::create();
* $browser = new Clue\React\Buzz\Browser($loop);
*
* $wsdl = '<?xml …';
* $options = array();
*
* $client = new Client($browser, $wsdl, $options);
* ```
*
* 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)
* to the [`Browser`](https://github.com/clue/reactphp-buzz#browser) instance:
*
* ```php
* $connector = new \React\Socket\Connector($loop, 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 Browser($loop, $connector);
* $client = new Client($browser, $wsdl);
* ```
*
* The `Client` works similar to PHP's `SoapClient` (which it uses under the
* hood), but leaves you the responsibility to load the WSDL file. This allows
* you to use local WSDL files, WSDL files from a cache or the most common form,
* downloading the WSDL file contents from an URL through the `Browser`:
*
* ```php
* $browser = new Browser($loop);
*
* $browser->get($url)->then(
* function (ResponseInterface $response) use ($browser) {
* // WSDL file is ready, create client
* $client = new Client($browser, (string)$response->getBody());
*
* // do something…
* },
* function (Exception $e) {
* // an error occured while trying to download the WSDL
* }
* );
* ```
*
* The `Client` constructor loads the given WSDL file contents into memory and
* parses its definition. If the given WSDL file is invalid and can not be
* parsed, this will throw a `SoapFault`:
*
* ```php
* try {
* $client = new Client($browser, $wsdl);
* } catch (SoapFault $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* }
* ```
*
* > Note that if you have `ext-xdebug` loaded, this may halt with a fatal
* error instead of throwing a `SoapFault`. It is not recommended to use this
* extension in production, so this should only ever affect test environments.
*
* The `Client` constructor accepts an array of options. All given options will
* be passed through to the underlying `SoapClient`. However, not all options
* make sense in this async implementation and as such may not have the desired
* effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php)
* documentation for more details.
*
* If working in WSDL mode, the `$options` parameter is optional. If working in
* non-WSDL mode, the WSDL parameter must be set to `null` and the options
* parameter must contain the `location` and `uri` options, where `location` is
* the URL of the SOAP server to send the request to, and `uri` is the target
* namespace of the SOAP service:
*
* ```php
* $client = new Client($browser, null, array(
* 'location' => 'http://example.com',
* 'uri' => 'http://ping.example.com',
* ));
* ```
*
* Similarly, if working in WSDL mode, the `location` option can be used to
* explicitly overwrite the URL of the SOAP server to send the request to:
*
* ```php
* $client = new Client($browser, $wsdl, array(
* 'location' => 'http://example.com'
* ));
* ```
*
* You can use the `soap_version` option to change from the default SOAP 1.1 to
* use SOAP 1.2 instead:
*
* ```php
* $client = new Client($browser, $wsdl, array(
* 'soap_version' => SOAP_1_2
* ));
* ```
*
* You can use the `classmap` option to map certain WSDL types to PHP classes
* like this:
*
* ```php
* $client = new Client($browser, $wsdl, array(
* 'classmap' => array(
* 'getBankResponseType' => BankResponse::class
* )
* ));
* ```
*
* The `proxy_host` option (and family) is not supported by this library. As an
* alternative, you can configure the given `$browser` instance to use an
* [HTTP proxy server](https://github.com/clue/reactphp-buzz#http-proxy).
* If you find any other option is missing or not supported here, PRs are much
* appreciated!
*
* All public methods of the `Client` are considered *advanced usage*.
* If you want to call RPC functions, see below for the [`Proxy`](#proxy) class.
*/
class Client
{
private $browser;
private $encoder;
private $decoder;
/**
* Instantiate a new SOAP client for the given WSDL contents.
*
* @param Browser $browser
* @param string|null $wsdlContents
* @param array $options
*/
public function __construct(Browser $browser, $wsdlContents, array $options = array())
{
$wsdl = $wsdlContents !== null ? 'data://text/plain;base64,' . base64_encode($wsdlContents) : null;
// Accept HTTP responses with error status codes as valid responses.
// This is done in order to process these error responses through the normal SOAP decoder.
// Additionally, we explicitly limit number of redirects to zero because following redirects makes little sense
// because it transforms the POST request to a GET one and hence loses the SOAP request body.
$browser = $browser->withOptions(array(
'obeySuccessCode' => false,
'followRedirects' => true,
'maxRedirects' => 0
));
$this->browser = $browser;
$this->encoder = new ClientEncoder($wsdl, $options);
$this->decoder = new ClientDecoder($wsdl, $options);
}
/**
* Queue the given function to be sent via SOAP and wait for a response from the remote web service.
*
* ```php
* // advanced usage, see Proxy for recommended alternative
* $promise = $client->soapCall('ping', array('hello', 42));
* ```
*
* Note: This is considered *advanced usage*, you may want to look into using the [`Proxy`](#proxy) instead.
*
* ```php
* $proxy = new Proxy($client);
* $promise = $proxy->ping('hello', 42);
* ```
*
* @param string $name
* @param mixed[] $args
* @return PromiseInterface Returns a Promise<mixed, Exception>
*/
public function soapCall($name, $args)
{
try {
$request = $this->encoder->encode($name, $args);
} catch (\Exception $e) {
$deferred = new Deferred();
$deferred->reject($e);
return $deferred->promise();
}
$decoder = $this->decoder;
return $this->browser->send($request)->then(
function (ResponseInterface $response) use ($decoder, $name) {
// HTTP response received => decode results for this function call
return $decoder->decode($name, (string)$response->getBody());
}
);
}
/**
* Returns an array of functions defined in the WSDL.
*
* It returns the equivalent of PHP's
* [`SoapClient::__getFunctions()`](http://php.net/manual/en/soapclient.getfunctions.php).
* In non-WSDL mode, this method returns `null`.
*
* @return string[]|null
*/
public function getFunctions()
{
return $this->encoder->__getFunctions();
}
/**
* Returns an array of types defined in the WSDL.
*
* It returns the equivalent of PHP's
* [`SoapClient::__getTypes()`](http://php.net/manual/en/soapclient.gettypes.php).
* In non-WSDL mode, this method returns `null`.
*
* @return string[]|null
*/
public function getTypes()
{
return $this->encoder->__getTypes();
}
/**
* Returns the location (URI) of the given webservice `$function`.
*
* Note that this is not to be confused with the WSDL file location.
* A WSDL file can contain any number of function definitions.
* It's very common that all of these functions use the same location definition.
* However, technically each function can potentially use a different location.
*
* The `$function` parameter should be a string with the the SOAP function name.
* See also [`getFunctions()`](#getfunctions) for a list of all available functions.
*
* ```php
* assert('http://example.com/soap/service' === $client->getLocation('echo'));
* ```
*
* For easier access, this function also accepts a numeric function index.
* It then uses [`getFunctions()`](#getfunctions) internally to get the function
* name for the given index.
* This is particularly useful for the very common case where all functions use the
* same location and accessing the first location is sufficient.
*
* ```php
* assert('http://example.com/soap/service' === $client->getLocation(0));
* ```
*
* When the `location` option has been set in the `Client` constructor
* (such as when in non-WSDL mode) or via the `withLocation()` method, this
* method returns the value of the given location.
*
* Passing a `$function` not defined in the WSDL file will throw a `SoapFault`.
*
* @param string|int $function
* @return string
* @throws \SoapFault if given function does not exist
* @see self::getFunctions()
*/
public function getLocation($function)
{
if (is_int($function)) {
$functions = $this->getFunctions();
if (isset($functions[$function]) && preg_match('/^\w+ (\w+)\(/', $functions[$function], $match)) {
$function = $match[1];
}
}
// encode request for given $function
return (string)$this->encoder->encode($function, array())->getUri();
}
/**
* Returns a new `Client` with the updated location (URI) for all functions.
*
* Note that this is not to be confused with the WSDL file location.
* A WSDL file can contain any number of function definitions.
* It's very common that all of these functions use the same location definition.
* However, technically each function can potentially use a different location.
*
* ```php
* $client = $client->withLocation('http://example.com/soap');
*
* assert('http://example.com/soap' === $client->getLocation('echo'));
* ```
*
* As an alternative to this method, you can also set the `location` option
* in the `Client` constructor (such as when in non-WSDL mode).
*
* @param string $location
* @return self
* @see self::getLocation()
*/
public function withLocation($location)
{
$client = clone $this;
$client->encoder = clone $this->encoder;
$client->encoder->__setLocation($location);
return $client;
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Clue\React\Soap\Protocol;
use \SoapClient;
/**
* @internal
*/
final class ClientDecoder extends SoapClient
{
private $response = null;
/**
* Decodes the SOAP response / return value from the given SOAP envelope (HTTP response body)
*
* @param string $function
* @param string $response
* @return mixed
* @throws \SoapFault if response indicates a fault (error condition) or is invalid
*/
public function decode($function, $response)
{
// Temporarily save response internally for further processing
$this->response = $response;
// Let's pretend we just invoked the given SOAP function.
// This won't actually invoke anything (see `__doRequest()`), but this
// requires a valid function name to match its definition in the WSDL.
// Internally, simply use the injected response to parse its results.
$ret = $this->__soapCall($function, array());
$this->response = null;
return $ret;
}
/**
* Overwrites the internal request logic to parse the response
*
* By overwriting this method, we can skip the actual request sending logic
* and still use the internal parsing logic by injecting the response as
* the return code in this method. This will implicitly be invoked by the
* call to `pseudoCall()` in the above `decode()` method.
*
* @see SoapClient::__doRequest()
*/
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
// the actual result doesn't actually matter, just return the given result
// this will be processed internally and will return the parsed result
return $this->response;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Clue\React\Soap\Protocol;
use \SoapClient;
use RingCentral\Psr7\Request;
/**
* @internal
*/
final class ClientEncoder extends SoapClient
{
private $request = null;
/**
* Encodes the given RPC function name and arguments as a SOAP request
*
* @param string $name
* @param array $args
* @return Request
* @throws \SoapFault if request is invalid according to WSDL
*/
public function encode($name, $args)
{
$this->__soapCall($name, $args);
$request = $this->request;
$this->request = null;
return $request;
}
/**
* Overwrites the internal request logic to build the request message
*
* By overwriting this method, we can skip the actual request sending logic
* and still use the internal request serializing logic by accessing the
* given `$request` parameter and building our custom request object from
* it. We skip/ignore its parsing logic by returing an empty response here.
* This will implicitly be invoked by the call to `__soapCall()` in the
* above `encode()` method.
*
* @see SoapClient::__doRequest()
*/
public function __doRequest($request, $location, $action, $version, $one_way = 0)
{
$headers = array();
if ($version === SOAP_1_1) {
$headers = array(
'SOAPAction' => $action,
'Content-Type' => 'text/xml; charset=utf-8'
);
} elseif ($version === SOAP_1_2) {
$headers = array(
'Content-Type' => 'application/soap+xml; charset=utf-8; action=' . $action
);
}
$this->request = new Request(
'POST',
(string)$location,
$headers,
(string)$request
);
// do not actually block here, just pretend we're done...
return '';
}
}

50
vendor/clue/soap-react/src/Proxy.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace Clue\React\Soap;
use React\Promise\PromiseInterface;
/**
* The `Proxy` class wraps an existing [`Client`](#client) instance in order to ease calling
* SOAP functions.
*
* ```php
* $proxy = new Proxy($client);
* ```
*
* Each and every method call to the `Proxy` class will be sent via SOAP.
*
* ```php
* $proxy->myMethod($myArg1, $myArg2)->then(function ($response) {
* // result received
* });
* ```
*
* Please refer to your WSDL or its accompanying documentation for details
* on which functions and arguments are supported.
*
* > Note that this class is called "Proxy" because it will forward (proxy) all
* method calls to the actual SOAP service via the underlying
* [`Client::soapCall()`](#soapcall) method. This is not to be confused with
* using a proxy server. See [`Client`](#client) documentation for more
* details on how to use an HTTP proxy server.
*/
final class Proxy
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
/**
* @param string $name
* @param mixed[] $args
* @return PromiseInterface
*/
public function __call($name, $args)
{
return $this->client->soapCall($name, $args);
}
}

96
vendor/clue/socket-raw/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,96 @@
# Changelog
## 1.6.0 (2022-04-14)
* Feature: Forward compatibility with PHP 8.1 release.
(#67 and #68 by @clue)
* Fix: Fix reporting refused connections on Windows.
(#69 by @clue)
* Improve CI setup and documentation.
(#70 and #65 by @clue, #64 by @szepeviktor and #66 by @PaulRotmann)
## 1.5.0 (2020-11-27)
* Feature: Support PHP 8 and drop legacy HHVM support.
(#60 and #61 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from export.
Update to PHPUnit 9 and simplify test matrix.
(#50, #51, #58 and #63 by @clue and #57 by @SimonFrings)
## 1.4.1 (2019-10-28)
* Fix: Fix error reporting when invoking methods on closed socket instance.
(#48 by @clue)
* Improve test suite to run tests on Windows via Travis CI.
(#49 by @clue)
## 1.4.0 (2019-01-22)
* Feature: Improve Windows support (async connections and Unix domain sockets).
(#43 by @clue)
* Improve test suite by adding forward compatibility with PHPUnit 7 and PHPUnit 6.
(#42 by @clue)
## 1.3.0 (2018-06-10)
* Feature: Add `$timeout` parameter for `Factory::createClient()`
(#39 by @Elbandi and @clue)
```php
// connect to Google, but wait no longer than 2.5s for connection
$socket = $factory->createClient('www.google.com:80', 2.5);
```
* Improve test suite by adding PHPUnit to require-dev,
update test suite to test against legacy PHP 5.3 through PHP 7.2 and
optionally skip functional integration tests requiring internet.
(#26 by @ascii-soup, #28, #29, #37 and #38 by @clue)
## 1.2.0 (2015-03-18)
* Feature: Expose optional `$type` parameter for `Socket::read()`
([#16](https://github.com/clue/php-socket-raw/pull/16) by @Elbandi)
## 1.1.0 (2014-10-24)
* Feature: Accept float timeouts like `0.5` for `Socket::selectRead()` and `Socket::selectWrite()`.
([#8](https://github.com/clue/php-socket-raw/issues/8))
* Feature: Add new `Socket::connectTimeout()` method.
([#11](https://github.com/clue/php-socket-raw/pull/11))
* Fix: Close invalid socket resource when `Factory` fails to create a `Socket`.
([#12](https://github.com/clue/php-socket-raw/pull/12))
* Fix: Calling `accept()` on an idle server socket emits right error code and message.
([#14](https://github.com/clue/php-socket-raw/pull/14))
## 1.0.0 (2014-05-10)
* Feature: Improved errors reporting through dedicated `Exception`
([#6](https://github.com/clue/socket-raw/pull/6))
* Feature: Support HHVM
([#5](https://github.com/clue/socket-raw/pull/5))
* Use PSR-4 layout
([#3](https://github.com/clue/socket-raw/pull/3))
* Continuous integration via Travis CI
## 0.1.2 (2013-05-09)
* Fix: The `Factory::createUdg()` now returns the right socket type.
* Fix: Fix ICMPv6 addressing to not require square brackets because it does not
use ports.
* Extended test suite.
## 0.1.1 (2013-04-18)
* Fix: Raw sockets now correctly report no port instead of a `0` port.
## 0.1.0 (2013-04-10)
* First tagged release

21
vendor/clue/socket-raw/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Christian Lück
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.

258
vendor/clue/socket-raw/README.md vendored Normal file
View File

@ -0,0 +1,258 @@
# clue/socket-raw
[![CI status](https://github.com/clue/socket-raw/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/socket-raw/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/clue/socket-raw?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/socket-raw)
Simple and lightweight OOP wrapper for PHP's low-level sockets extension (ext-sockets).
PHP offers two networking APIs, the newer [streams API](https://www.php.net/manual/en/book.stream.php) and the older [socket API](https://www.php.net/manual/en/ref.sockets.php).
While the former has been a huge step forward in generalizing various streaming resources,
it lacks some of the advanced features of the original and much more low-level socket API.
This lightweight library exposes this socket API in a modern way by providing a thin wrapper around the underlying API.
* **Full socket API** -
It exposes the whole [socket API](https://www.php.net/manual/en/ref.sockets.php) through a *sane* object-oriented interface.
Provides convenience methods for common operations as well as exposing all underlying methods and options.
* **Fluent interface** -
Uses a fluent interface so you can easily chain method calls.
Error conditions will be signalled using `Exception`s instead of relying on cumbersome return codes.
* **Lightweight, SOLID design** -
Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough)
and does not get in your way.
This library is merely a very thin wrapper and has no other external dependencies.
* **Good test coverage** -
Comes with an automated test suite and is regularly tested in the *real world*.
**Table of contents**
* [Support us](#support-us)
* [Quickstart example](#quickstart-example)
* [Usage](#usage)
* [Factory](#factory)
* [createClient()](#createclient)
* [createServer()](#createserver)
* [create*()](#create)
* [Socket](#socket)
* [Methods](#methods)
* [Data I/O](#data-io)
* [Unconnected I/O](#unconnected-io)
* [Non-blocking (async) I/O](#non-blocking-async-io)
* [Connection handling](#connection-handling)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
## Support us
We invest a lot of time developing, maintaining and updating our awesome
open-source projects. You can help us sustain this high-quality of our work by
[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get
numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue)
for details.
Let's take these projects to the next level together! 🚀
## Quickstart example
Once [installed](#install), you can use the following example to send and receive HTTP messages:
```php
$factory = new \Socket\Raw\Factory();
$socket = $factory->createClient('www.google.com:80');
echo 'Connected to ' . $socket->getPeerName() . PHP_EOL;
// send simple HTTP request to remote side
$socket->write("GET / HTTP/1.1\r\n\Host: www.google.com\r\n\r\n");
// receive and dump HTTP response
var_dump($socket->read(8192));
$socket->close();
```
See also the [examples](examples).
## Usage
### Factory
As shown in the [quickstart example](#quickstart-example), this library uses a `Factory` pattern
as a simple API to [`socket_create()`](https://www.php.net/manual/en/function.socket-create.php).
It provides simple access to creating TCP, UDP, UNIX, UDG and ICMP protocol sockets and supports both IPv4 and IPv6 addressing.
```php
$factory = new \Socket\Raw\Factory();
```
#### createClient()
The `createClient(string $address, null|float $timeout): Socket` method is
the most convenient method for creating connected client sockets
(similar to how [`fsockopen()`](https://www.php.net/manual/en/function.fsockopen.php) or
[`stream_socket_client()`](https://www.php.net/manual/en/function.stream-socket-client.php) work).
```php
// establish a TCP/IP stream connection socket to www.google.com on port 80
$socket = $factory->createClient('tcp://www.google.com:80');
// same as above, as scheme defaults to TCP
$socket = $factory->createClient('www.google.com:80');
// same as above, but wait no longer than 2.5s for connection
$socket = $factory->createClient('www.google.com:80', 2.5);
// create connectionless UDP/IP datagram socket connected to google's DNS
$socket = $factory->createClient('udp://8.8.8.8:53');
// establish TCP/IPv6 stream connection socket to localhost on port 1337
$socket = $factory->createClient('tcp://[::1]:1337');
// connect to local Unix stream socket path
$socket = $factory->createClient('unix:///tmp/daemon.sock');
// create Unix datagram socket
$socket = $factory->createClient('udg:///tmp/udg.socket');
// create a raw low-level ICMP socket (requires root!)
$socket = $factory->createClient('icmp://192.168.0.1');
```
#### createServer()
The `createServer($address)` method can be used to create a server side (listening) socket bound to specific address/path
(similar to how [`stream_socket_server()`](https://www.php.net/manual/en/function.stream-socket-server.php) works).
It accepts the same addressing scheme as the [`createClient()`](#createclient) method.
```php
// create a TCP/IP stream connection socket server on port 1337
$socket = $factory->createServer('tcp://localhost:1337');
// create a UDP/IPv6 datagram socket server on port 1337
$socket = $factory->createServer('udp://[::1]:1337');
```
#### create*()
Less commonly used, the `Factory` provides access to creating (unconnected) sockets for various socket types:
```php
$socket = $factory->createTcp4();
$socket = $factory->createTcp6();
$socket = $factory->createUdp4();
$socket = $factory->createUdp6();
$socket = $factory->createUnix();
$socket = $factory->createUdg();
$socket = $factory->createIcmp4();
$socket = $factory->createIcmp6();
```
You can also create arbitrary socket protocol types through the underlying mechanism:
```php
$factory->create($family, $type, $protocol);
```
### Socket
As discussed above, the `Socket` class is merely an object-oriented wrapper around a socket resource. As such, it helps if you're familar with socket programming in general.
The recommended way to create a `Socket` instance is via the above [`Factory`](#factory).
#### Methods
All low-level socket operations are available as methods on the `Socket` class.
You can refer to PHP's fairly good [socket API documentation](https://www.php.net/manual/en/ref.sockets.php) or the docblock comments in the [`Socket` class](src/Socket.php) to get you started.
##### Data I/O:
```
$socket->write('data');
$data = $socket->read(8192);
```
##### Unconnected I/O:
```
$socket->sendTo('data', $flags, $remote);
$data = $socket->rcvFrom(8192, $flags, $remote);
```
##### Non-blocking (async) I/O:
```
$socket->setBlocking(false);
$socket->selectRead();
$socket->selectWrite();
```
##### Connection handling:
```php
$client = $socket->accept();
$socket->bind($address);
$socket->connect($address);
$socket->shutdown();
$socket->close();
```
## 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 clue/socket-raw:^1.6
```
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 besides `ext-sockets` and supports running on legacy PHP 5.3 through
current PHP 8+.
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
```
Note that the test suite contains tests for ICMP sockets which require root
access on Unix/Linux systems. Therefor some tests will be skipped unless you run
the following command to execute the full test suite:
```bash
$ sudo 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
This project is released under the permissive [MIT license](LICENSE).
> Did you know that I offer custom development services and issuing invoices for
sponsorships of releases and for contributions? Contact me (@clue) for details.

23
vendor/clue/socket-raw/composer.json vendored Normal file
View File

@ -0,0 +1,23 @@
{
"name": "clue/socket-raw",
"description": "Simple and lightweight OOP wrapper for PHP's low-level sockets extension (ext-sockets).",
"keywords": ["socket", "stream", "datagram", "dgram", "client", "server", "ipv6", "tcp", "udp", "icmp", "unix", "udg"],
"homepage": "https://github.com/clue/socket-raw",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"autoload": {
"psr-4": {"Socket\\Raw\\": "src"}
},
"require": {
"ext-sockets": "*",
"php": ">=5.3"
},
"require-dev": {
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35"
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Socket\Raw;
use RuntimeException;
class Exception extends RuntimeException
{
/**
* Create an Exception after a socket operation on the given $resource failed
*
* @param \Socket|resource $resource
* @param string $messagePrefix
* @return self
* @uses socket_last_error() to get last socket error code
* @uses socket_clear_error() to clear socket error code
* @uses self::createFromCode() to automatically construct exception with full error message
*/
public static function createFromSocketResource($resource, $messagePrefix = 'Socket operation failed')
{
if (PHP_VERSION_ID >= 80000) {
try {
$code = socket_last_error($resource);
} catch (\Error $e) {
$code = SOCKET_ENOTSOCK;
}
} elseif (is_resource($resource)) {
$code = socket_last_error($resource);
socket_clear_error($resource);
} else {
// socket already closed, return fixed error code instead of operating on invalid handle
$code = SOCKET_ENOTSOCK;
}
return self::createFromCode($code, $messagePrefix);
}
/**
* Create an Exception after a global socket operation failed (like socket creation)
*
* @param string $messagePrefix
* @return self
* @uses socket_last_error() to get last global error code
* @uses socket_clear_error() to clear global error code
* @uses self::createFromCode() to automatically construct exception with full error message
*/
public static function createFromGlobalSocketOperation($messagePrefix = 'Socket operation failed')
{
$code = socket_last_error();
socket_clear_error();
return self::createFromCode($code, $messagePrefix);
}
/**
* Create an Exception for given error $code
*
* @param int $code
* @param string $messagePrefix
* @return self
* @throws Exception if given $val is boolean false
* @uses self::getErrorMessage() to translate error code to error message
*/
public static function createFromCode($code, $messagePrefix = 'Socket error')
{
return new self($messagePrefix . ': ' . self::getErrorMessage($code), $code);
}
/**
* get error message for given error code
*
* @param int $code error code
* @return string
* @uses socket_strerror() to translate error code to error message
* @uses get_defined_constants() to check for related error constant
*/
protected static function getErrorMessage($code)
{
$string = socket_strerror($code);
// search constant starting with SOCKET_ for this error code
foreach (get_defined_constants() as $key => $value) {
if($value === $code && strpos($key, 'SOCKET_') === 0) {
$string .= ' (' . $key . ')';
break;
}
}
return $string;
}
}

282
vendor/clue/socket-raw/src/Factory.php vendored Normal file
View File

@ -0,0 +1,282 @@
<?php
namespace Socket\Raw;
use \InvalidArgumentException;
class Factory
{
/**
* create client socket connected to given target address
*
* @param string $address target address to connect to
* @param null|float $timeout connection timeout (in seconds), default null = no limit
* @return \Socket\Raw\Socket
* @throws InvalidArgumentException if given address is invalid
* @throws Exception on error
* @uses self::createFromString()
* @uses Socket::connect()
* @uses Socket::connectTimeout()
*/
public function createClient($address, $timeout = null)
{
$socket = $this->createFromString($address, $scheme);
try {
if ($timeout === null) {
$socket->connect($address);
} else {
// connectTimeout enables non-blocking mode, so turn blocking on again
$socket->connectTimeout($address, $timeout);
$socket->setBlocking(true);
}
} catch (Exception $e) {
$socket->close();
throw $e;
}
return $socket;
}
/**
* create server socket bound to given address (and start listening for streaming clients to connect to this stream socket)
*
* @param string $address address to bind socket to
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::createFromString()
* @uses Socket::bind()
* @uses Socket::listen() only for stream sockets (TCP/UNIX)
*/
public function createServer($address)
{
$socket = $this->createFromString($address, $scheme);
try {
$socket->bind($address);
if ($socket->getType() === SOCK_STREAM) {
$socket->listen();
}
} catch (Exception $e) {
$socket->close();
throw $e;
}
return $socket;
}
/**
* create TCP/IPv4 stream socket
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createTcp4()
{
return $this->create(AF_INET, SOCK_STREAM, SOL_TCP);
}
/**
* create TCP/IPv6 stream socket
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createTcp6()
{
return $this->create(AF_INET6, SOCK_STREAM, SOL_TCP);
}
/**
* create UDP/IPv4 datagram socket
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createUdp4()
{
return $this->create(AF_INET, SOCK_DGRAM, SOL_UDP);
}
/**
* create UDP/IPv6 datagram socket
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createUdp6()
{
return $this->create(AF_INET6, SOCK_DGRAM, SOL_UDP);
}
/**
* create local UNIX stream socket
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createUnix()
{
return $this->create(AF_UNIX, SOCK_STREAM, 0);
}
/**
* create local UNIX datagram socket (UDG)
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createUdg()
{
return $this->create(AF_UNIX, SOCK_DGRAM, 0);
}
/**
* create raw ICMP/IPv4 datagram socket (requires root!)
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createIcmp4()
{
return $this->create(AF_INET, SOCK_RAW, getprotobyname('icmp'));
}
/**
* create raw ICMPv6 (IPv6) datagram socket (requires root!)
*
* @return \Socket\Raw\Socket
* @throws Exception on error
* @uses self::create()
*/
public function createIcmp6()
{
return $this->create(AF_INET6, SOCK_RAW, 58 /*getprotobyname('icmp')*/);
}
/**
* create low level socket with given arguments
*
* @param int $domain
* @param int $type
* @param int $protocol
* @return \Socket\Raw\Socket
* @throws Exception if creating socket fails
* @throws \Error PHP 8 only: throws \Error when arguments are invalid
* @uses socket_create()
*/
public function create($domain, $type, $protocol)
{
$sock = @socket_create($domain, $type, $protocol);
if ($sock === false) {
throw Exception::createFromGlobalSocketOperation('Unable to create socket');
}
return new Socket($sock);
}
/**
* create a pair of indistinguishable sockets (commonly used in IPC)
*
* @param int $domain
* @param int $type
* @param int $protocol
* @return \Socket\Raw\Socket[]
* @throws Exception if creating pair of sockets fails
* @throws \Error PHP 8 only: throws \Error when arguments are invalid
* @uses socket_create_pair()
*/
public function createPair($domain, $type, $protocol)
{
$ret = @socket_create_pair($domain, $type, $protocol, $pair);
if ($ret === false) {
throw Exception::createFromGlobalSocketOperation('Unable to create pair of sockets');
}
return array(new Socket($pair[0]), new Socket($pair[1]));
}
/**
* create TCP/IPv4 stream socket and listen for new connections
*
* @param int $port
* @param int $backlog
* @return \Socket\Raw\Socket
* @throws Exception if creating listening socket fails
* @throws \Error PHP 8 only: throws \Error when arguments are invalid
* @uses socket_create_listen()
* @see self::createServer() as an alternative to bind to specific IP, IPv6, UDP, UNIX, UGP
*/
public function createListen($port, $backlog = 128)
{
$sock = @socket_create_listen($port, $backlog);
if ($sock === false) {
throw Exception::createFromGlobalSocketOperation('Unable to create listening socket');
}
return new Socket($sock);
}
/**
* create socket for given address
*
* @param string $address (passed by reference in order to remove scheme, if present)
* @param string|null $scheme default scheme to use, defaults to TCP (passed by reference in order to update with actual scheme used)
* @return \Socket\Raw\Socket
* @throws InvalidArgumentException if given address is invalid
* @throws Exception in case creating socket failed
* @uses self::createTcp4() etc.
*/
public function createFromString(&$address, &$scheme)
{
if ($scheme === null) {
$scheme = 'tcp';
}
$hasScheme = false;
$pos = strpos($address, '://');
if ($pos !== false) {
$scheme = substr($address, 0, $pos);
$address = substr($address, $pos + 3);
$hasScheme = true;
}
if (strpos($address, ':') !== strrpos($address, ':') && in_array($scheme, array('tcp', 'udp', 'icmp'))) {
// TCP/UDP/ICMP address with several colons => must be IPv6
$scheme .= '6';
}
if ($scheme === 'tcp') {
$socket = $this->createTcp4();
} elseif ($scheme === 'udp') {
$socket = $this->createUdp4();
} elseif ($scheme === 'tcp6') {
$socket = $this->createTcp6();
} elseif ($scheme === 'udp6') {
$socket = $this->createUdp6();
} elseif ($scheme === 'unix') {
$socket = $this->createUnix();
} elseif ($scheme === 'udg') {
$socket = $this->createUdg();
} elseif ($scheme === 'icmp') {
$socket = $this->createIcmp4();
} elseif ($scheme === 'icmp6') {
$socket = $this->createIcmp6();
if ($hasScheme) {
// scheme was stripped from address, resulting IPv6 must not
// have a port (due to ICMP) and thus must not be enclosed in
// square brackets
$address = trim($address, '[]');
}
} else {
throw new InvalidArgumentException('Invalid address scheme given');
}
return $socket;
}
}

562
vendor/clue/socket-raw/src/Socket.php vendored Normal file
View File

@ -0,0 +1,562 @@
<?php
namespace Socket\Raw;
/**
* Simple and lightweight OOP wrapper for the low-level sockets extension (ext-sockets)
*
* @author clue
* @link https://github.com/clue/php-socket-raw
*/
class Socket
{
/**
* reference to actual socket resource
*
* @var \Socket|resource
*/
private $resource;
/**
* instanciate socket wrapper for given socket resource
*
* should usually not be called manually, see Factory
*
* @param \Socket|resource $resource
* @see Factory as the preferred (and simplest) way to construct socket instances
*/
public function __construct($resource)
{
$this->resource = $resource;
}
/**
* get actual socket resource
*
* @return \Socket|resource returns the socket resource (a `Socket` object as of PHP 8)
*/
public function getResource()
{
return $this->resource;
}
/**
* accept an incomming connection on this listening socket
*
* @return \Socket\Raw\Socket new connected socket used for communication
* @throws Exception on error, if this is not a listening socket or there's no connection pending
* @throws \Error PHP 8 only: throws \Error when socket is invalid
* @see self::selectRead() to check if this listening socket can accept()
* @see Factory::createServer() to create a listening socket
* @see self::listen() has to be called first
* @uses socket_accept()
*/
public function accept()
{
$resource = @socket_accept($this->resource);
if ($resource === false) {
throw Exception::createFromGlobalSocketOperation();
}
return new Socket($resource);
}
/**
* binds a name/address/path to this socket
*
* has to be called before issuing connect() or listen()
*
* @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses socket_bind()
*/
public function bind($address)
{
$ret = @socket_bind($this->resource, $this->unformatAddress($address, $port), $port);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this;
}
/**
* close this socket
*
* ATTENTION: make sure to NOT re-use this socket instance after closing it!
* its socket resource remains closed and most further operations will fail!
*
* @return self $this (chainable)
* @throws \Error PHP 8 only: throws \Error when socket is invalid
* @see self::shutdown() should be called before closing socket
* @uses socket_close()
*/
public function close()
{
socket_close($this->resource);
return $this;
}
/**
* initiate a connection to given address
*
* @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses socket_connect()
*/
public function connect($address)
{
$ret = @socket_connect($this->resource, $this->unformatAddress($address, $port), $port);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this;
}
/**
* Initiates a new connection to given address, wait for up to $timeout seconds
*
* The given $timeout parameter is an upper bound, a maximum time to wait
* for the connection to be either accepted or rejected.
*
* The resulting socket resource will be set to non-blocking mode,
* regardless of its previous state and whether this method succedes or
* if it fails. Make sure to reset with `setBlocking(true)` if you want to
* continue using blocking calls.
*
* @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
* @param float $timeout maximum time to wait (in seconds)
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses self::setBlocking() to enable non-blocking mode
* @uses self::connect() to initiate the connection
* @uses self::selectWrite() to wait for the connection to complete
* @uses self::assertAlive() to check connection state
*/
public function connectTimeout($address, $timeout)
{
$this->setBlocking(false);
try {
// socket is non-blocking, so connect should emit EINPROGRESS
$this->connect($address);
// socket is already connected immediately?
return $this;
} catch (Exception $e) {
// non-blocking connect() should be EINPROGRESS (or EWOULDBLOCK on Windows) => otherwise re-throw
if ($e->getCode() !== SOCKET_EINPROGRESS && $e->getCode() !== SOCKET_EWOULDBLOCK) {
throw $e;
}
// connection should be completed (or rejected) within timeout: socket becomes writable on success or error
// Windows requires special care because it uses exceptfds for socket errors: https://github.com/reactphp/event-loop/issues/206
$r = null;
$w = array($this->resource);
$e = DIRECTORY_SEPARATOR === '\\' ? $w : null;
$ret = @socket_select($r, $w, $e, $timeout === null ? null : (int) $timeout, (int) (($timeout - floor($timeout)) * 1000000));
if ($ret === false) {
throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
} elseif ($ret === 0) {
throw new Exception('Timed out while waiting for connection', SOCKET_ETIMEDOUT);
}
// confirm connection success (or fail if connected has been rejected)
$this->assertAlive();
return $this;
}
}
/**
* get socket option
*
* @param int $level
* @param int $optname
* @return mixed
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses socket_get_option()
*/
public function getOption($level, $optname)
{
$value = @socket_get_option($this->resource, $level, $optname);
if ($value === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $value;
}
/**
* get remote side's address/path
*
* @return string
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket is invalid
* @uses socket_getpeername()
*/
public function getPeerName()
{
$ret = @socket_getpeername($this->resource, $address, $port);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this->formatAddress($address, $port);
}
/**
* get local side's address/path
*
* @return string
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket is invalid
* @uses socket_getsockname()
*/
public function getSockName()
{
$ret = @socket_getsockname($this->resource, $address, $port);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this->formatAddress($address, $port);
}
/**
* start listen for incoming connections
*
* @param int $backlog maximum number of incoming connections to be queued
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::bind() has to be called first to bind name to socket
* @uses socket_listen()
*/
public function listen($backlog = 0)
{
$ret = @socket_listen($this->resource, $backlog);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this;
}
/**
* read up to $length bytes from connect()ed / accept()ed socket
*
* The $type parameter specifies if this should use either binary safe reading
* (PHP_BINARY_READ, the default) or stop at CR or LF characters (PHP_NORMAL_READ)
*
* @param int $length maximum length to read
* @param int $type either of PHP_BINARY_READ (the default) or PHP_NORMAL_READ
* @return string
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::recv() if you need to pass flags
* @uses socket_read()
*/
public function read($length, $type = PHP_BINARY_READ)
{
$data = @socket_read($this->resource, $length, $type);
if ($data === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $data;
}
/**
* receive up to $length bytes from connect()ed / accept()ed socket
*
* @param int $length maximum length to read
* @param int $flags
* @return string
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::read() if you do not need to pass $flags
* @see self::recvFrom() if your socket is not connect()ed
* @uses socket_recv()
*/
public function recv($length, $flags)
{
$ret = @socket_recv($this->resource, $buffer, $length, $flags);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $buffer;
}
/**
* receive up to $length bytes from socket
*
* @param int $length maximum length to read
* @param int $flags
* @param string $remote reference will be filled with remote/peer address/path
* @return string
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::recv() if your socket is connect()ed
* @uses socket_recvfrom()
*/
public function recvFrom($length, $flags, &$remote)
{
$ret = @socket_recvfrom($this->resource, $buffer, $length, $flags, $address, $port);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
$remote = $this->formatAddress($address, $port);
return $buffer;
}
/**
* check socket to see if a read/recv/revFrom will not block
*
* @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
* @return boolean true = socket ready (read will not block), false = timeout expired, socket is not ready
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses socket_select()
*/
public function selectRead($sec = 0)
{
$usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
$r = array($this->resource);
$n = null;
$ret = @socket_select($r, $n, $n, $sec === null ? null : (int) $sec, $usec);
if ($ret === false) {
throw Exception::createFromGlobalSocketOperation('Failed to select socket for reading');
}
return !!$ret;
}
/**
* check socket to see if a write/send/sendTo will not block
*
* @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
* @return boolean true = socket ready (write will not block), false = timeout expired, socket is not ready
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses socket_select()
*/
public function selectWrite($sec = 0)
{
$usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
$w = array($this->resource);
$n = null;
$ret = @socket_select($n, $w, $n, $sec === null ? null : (int) $sec, $usec);
if ($ret === false) {
throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
}
return !!$ret;
}
/**
* send given $buffer to connect()ed / accept()ed socket
*
* @param string $buffer
* @param int $flags
* @return int number of bytes actually written (make sure to check against given buffer length!)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::write() if you do not need to pass $flags
* @see self::sendTo() if your socket is not connect()ed
* @uses socket_send()
*/
public function send($buffer, $flags)
{
$ret = @socket_send($this->resource, $buffer, strlen($buffer), $flags);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $ret;
}
/**
* send given $buffer to socket
*
* @param string $buffer
* @param int $flags
* @param string $remote remote/peer address/path
* @return int number of bytes actually written
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::send() if your socket is connect()ed
* @uses socket_sendto()
*/
public function sendTo($buffer, $flags, $remote)
{
$ret = @socket_sendto($this->resource, $buffer, strlen($buffer), $flags, $this->unformatAddress($remote, $port), $port);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $ret;
}
/**
* enable/disable blocking/nonblocking mode (O_NONBLOCK flag)
*
* @param boolean $toggle
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @uses socket_set_block()
* @uses socket_set_nonblock()
*/
public function setBlocking($toggle = true)
{
$ret = $toggle ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this;
}
/**
* set socket option
*
* @param int $level
* @param int $optname
* @param mixed $optval
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::getOption()
* @uses socket_set_option()
*/
public function setOption($level, $optname, $optval)
{
$ret = @socket_set_option($this->resource, $level, $optname, $optval);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this;
}
/**
* shuts down socket for receiving, sending or both
*
* @param int $how 0 = shutdown reading, 1 = shutdown writing, 2 = shutdown reading and writing
* @return self $this (chainable)
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::close()
* @uses socket_shutdown()
*/
public function shutdown($how = 2)
{
$ret = @socket_shutdown($this->resource, $how);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $this;
}
/**
* write $buffer to connect()ed / accept()ed socket
*
* @param string $buffer
* @return int number of bytes actually written
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
* @see self::send() if you need to pass flags
* @uses socket_write()
*/
public function write($buffer)
{
$ret = @socket_write($this->resource, $buffer);
if ($ret === false) {
throw Exception::createFromSocketResource($this->resource);
}
return $ret;
}
/**
* get socket type as passed to socket_create()
*
* @return int usually either SOCK_STREAM or SOCK_DGRAM
* @throws Exception on error
* @throws \Error PHP 8 only: throws \Error when socket is invalid
* @uses self::getOption()
*/
public function getType()
{
return $this->getOption(SOL_SOCKET, SO_TYPE);
}
/**
* assert that this socket is alive and its error code is 0
*
* This will fetch and reset the current socket error code from the
* socket and options and will throw an Exception along with error
* message and code if the code is not 0, i.e. if it does indicate
* an error situation.
*
* Calling this method should not be needed in most cases and is
* likely to not throw an Exception. Each socket operation like
* connect(), send(), etc. will throw a dedicated Exception in case
* of an error anyway.
*
* @return self $this (chainable)
* @throws Exception if error code is not 0
* @throws \Error PHP 8 only: throws \Error when socket is invalid
* @uses self::getOption() to retrieve and clear current error code
* @uses self::getErrorMessage() to translate error code to
*/
public function assertAlive()
{
$code = $this->getOption(SOL_SOCKET, SO_ERROR);
if ($code !== 0) {
throw Exception::createFromCode($code, 'Socket error');
}
return $this;
}
/**
* format given address/host/path and port
*
* @param string $address
* @param int $port
* @return string
*/
protected function formatAddress($address, $port)
{
if ($port !== 0) {
if (strpos($address, ':') !== false) {
$address = '[' . $address . ']';
}
$address .= ':' . $port;
}
return $address;
}
/**
* format given address by splitting it into returned address and port set by reference
*
* @param string $address
* @param int $port
* @return string address with port removed
*/
protected function unformatAddress($address, &$port)
{
// [::1]:2 => ::1 2
// test:2 => test 2
// ::1 => ::1
// test => test
$colon = strrpos($address, ':');
// there is a colon and this is the only colon or there's a closing IPv6 bracket right before it
if ($colon !== false && (strpos($address, ':') === $colon || strpos($address, ']') === ($colon - 1))) {
$port = (int)substr($address, $colon + 1);
$address = substr($address, 0, $colon);
// remove IPv6 square brackets
if (substr($address, 0, 1) === '[') {
$address = substr($address, 1, -1);
}
}
return $address;
}
}

481
vendor/clue/socks-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,481 @@
# Changelog
## 1.4.0 (2022-08-31)
* Feature: Full support for PHP 8.1 and PHP 8.2.
(#105 by @clue and #108 by @SimonFrings)
* Feature: Mark passwords and URIs as `#[\SensitiveParameter]` (PHP 8.2+).
(#109 by @SimonFrings)
* Feature: Forward compatibility with upcoming Promise v3.
(#106 by @clue)
* Bug: Fix invalid references in exception stack trace.
(#104 by @clue)
* Improve test suite and fix legacy HHVM build.
(#107 by @SimonFrings)
## 1.3.0 (2021-08-06)
* Feature: Simplify usage by supporting new default loop and making `Connector` optional.
(#100 and #101 by @clue)
```php
// old (still supported)
$proxy = new Clue\React\Socks\Client(
$url,
new React\Socket\Connector($loop)
);
$server = new Clue\React\Socks\Server($loop);
$server->listen(new React\Socket\Server('127.0.0.1:1080', $loop));
// new (using default loop)
$proxy = new Clue\React\Socks\Client('127.0.0.1:1080');
$socks = new Clue\React\Socks\Server();
$socks->listen(new React\Socket\SocketServer('127.0.0.1:1080'));
```
* Documentation improvements and updated examples.
(#98 and #102 by @clue and #99 by @PaulRotmann)
* Improve test suite and use GitHub actions for continuous integration (CI).
(#97 by @SimonFrings)
## 1.2.0 (2020-10-23)
* Enhanced documentation for ReactPHP's new HTTP client.
(#95 by @SimonFrings)
* Improve test suite, prepare PHP 8 support and support PHPUnit 9.3.
(#96 by @SimonFrings)
## 1.1.0 (2020-06-19)
* Feature / Fix: Support PHP 7.4 by skipping unneeded cleanup of exception trace args.
(#92 by @clue)
* Clean up test suite and add `.gitattributes` to exclude dev files from exports.
Run tests on PHP 7.4, PHPUnit 9 and simplify test matrix.
Link to using SSH proxy (SSH tunnel) as an alternative.
(#88 by @clue and #91 and #93 by @SimonFrings)
## 1.0.0 (2018-11-20)
* First stable release, now following SemVer!
* Feature / BC break: Unify SOCKS5 and SOCKS4(a) protocol version handling,
the `Client` now defaults to SOCKS5 instead of SOCKS4a,
remove explicit SOCKS4a handling and merge into SOCKS4 protocol handling and
URI scheme `socks5://` now only acts as an alias for default `socks://` scheme.
(#74, #81 and #87 by @clue)
```php
// old: defaults to SOCKS4a
$client = new Client('127.0.0.1:1080', $connector);
$client = new Client('socks://127.0.0.1:1080', $connector);
// new: defaults to SOCKS5
$client = new Client('127.0.0.1:1080', $connector);
$client = new Client('socks://127.0.0.1:1080', $connector);
// new: explicitly use legacy SOCKS4(a)
$client = new Client('socks4://127.0.0.1:1080', $connector);
// unchanged: explicitly use SOCKS5
$client = new Client('socks5://127.0.0.1:1080', $connector);
```
* Feature / BC break: Clean up `Server` interface,
add `Server::listen()` method instead of accepting socket in constructor,
replace `Server::setAuth()` with optional constructor parameter,
remove undocumented "connection" event from Server and drop explicit Evenement dependency and
mark all classes as `final` and all internal APIs as `@internal`
(#78, #79, #80 and #84 by @clue)
```php
// old: socket passed to server constructor
$socket = new React\Socket\Server(1080, $loop);
$server = new Clue\React\Socks\Server($loop, $socket);
// old: authentication via setAuthArray()/setAuth() methods
$server = new Clue\React\Socks\Server($loop, $socket);
$server->setAuthArray(array(
'tom' => 'password',
'admin' => 'root'
));
// new: socket passed to listen() method
$server = new Clue\React\Socks\Server($loop);
$socket = new React\Socket\Server(1080, $loop);
$server->listen($socket);
// new: authentication passed to server constructor
$server = new Clue\React\Socks\Server($loop, null, array(
'tom' => 'password',
'admin' => 'root'
));
$server->listen($socket);
```
* Feature: Improve error reporting for failed connections attempts by always including target URI in exceptions and
improve promise cancellation and clean up any garbage references.
(#82 and #83 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.
Similarly, any underlying connection issues to the proxy server will now be
reported as part of the previous exception.
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 = $proxy->connect('tcp://example.com:80');
$promise->then(function (ConnectionInterface $connection) {
// …
}, function (Exception $e) {
echo $e->getMessage();
});
```
* Improve documentation and examples, link to other projects and update project homepage.
(#73, #75 and #85 by @clue)
## 0.8.7 (2017-12-17)
* Feature: Support SOCKS over TLS (`sockss://` URI scheme)
(#70 and #71 by @clue)
```php
// new: now supports SOCKS over TLS
$client = new Client('socks5s://localhost', $connector);
```
* Feature: Support communication over Unix domain sockets (UDS)
(#69 by @clue)
```php
// new: now supports SOCKS over Unix domain sockets (UDS)
$client = new Client('socks5+unix:///tmp/proxy.sock', $connector);
```
* Improve test suite by adding forward compatibility with PHPUnit 6
(#68 by @clue)
## 0.8.6 (2017-09-17)
* Feature: Forward compatibility with Evenement v3.0
(#67 by @WyriHaximus)
## 0.8.5 (2017-09-01)
* Feature: Use socket error codes for connection rejections
(#63 by @clue)
```php
$promise = $proxy->connect('imap.example.com:143');
$promise->then(null, function (Exeption $e) {
if ($e->getCode() === SOCKET_EACCES) {
echo 'Failed to authenticate with proxy!';
}
throw $e;
});
```
* Feature: Report matching SOCKS5 error codes for server side connection errors
(#62 by @clue)
* Fix: Fix SOCKS5 client receiving destination hostnames and
fix IPv6 addresses as hostnames for TLS certificates
(#64 and #65 by @clue)
* Improve test suite by locking Travis distro so new defaults will not break the build and
optionally exclude tests that rely on working internet connection
(#61 and #66 by @clue)
## 0.8.4 (2017-07-27)
* Feature: Server now passes client source address to Connector
(#60 by @clue)
## 0.8.3 (2017-07-18)
* Feature: Pass full remote URI as parameter to authentication callback
(#58 by @clue)
```php
// new third parameter passed to authentication callback
$server->setAuth(function ($user, $pass, $remote) {
$ip = parse_url($remote, PHP_URL_HOST);
return ($ip === '127.0.0.1');
});
```
* Fix: Fix connecting to IPv6 address via SOCKS5 server and validate target
URI so hostname can not contain excessive URI components
(#59 by @clue)
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors
(#57 by @clue)
## 0.8.2 (2017-05-09)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#56 by @clue)
## 0.8.1 (2017-04-21)
* Update examples to use URIs with default port 1080 and accept proxy URI arguments
(#54 by @clue)
* Remove now unneeded dependency on `react/stream`
(#55 by @clue)
## 0.8.0 (2017-04-18)
* Feature: Merge `Server` class from clue/socks-server
(#52 by @clue)
```php
$socket = new React\Socket\Server(1080, $loop);
$server = new Clue\React\Socks\Server($loop, $socket);
```
> Upgrading from [clue/socks-server](https://github.com/clue/php-socks-server)?
The classes have been moved as-is, so you can simply start using the new
class name `Clue\React\Socks\Server` with no other changes required.
## 0.7.0 (2017-04-14)
* Feature / BC break: Replace depreacted SocketClient with Socket v0.7 and
use `connect($uri)` instead of `create($host, $port)`
(#51 by @clue)
```php
// old
$connector = new React\SocketClient\TcpConnector($loop);
$client = new Client(1080, $connector);
$client->create('google.com', 80)->then(function (Stream $conn) {
$conn->write("…");
});
// new
$connector = new React\Socket\TcpConnector($loop);
$client = new Client(1080, $connector);
$client->connect('google.com:80')->then(function (ConnectionInterface $conn) {
$conn->write("…");
});
```
* Improve test suite by adding PHPUnit to require-dev
(#50 by @clue)
## 0.6.0 (2016-11-29)
* Feature / BC break: Pass connector into `Client` instead of loop, remove unneeded deps
(#49 by @clue)
```php
// old (connector is create implicitly)
$client = new Client('127.0.0.1', $loop);
// old (connector can optionally be passed)
$client = new Client('127.0.0.1', $loop, $connector);
// new (connector is now mandatory)
$connector = new React\SocketClient\TcpConnector($loop);
$client = new Client('127.0.0.1', $connector);
```
* Feature / BC break: `Client` now implements `ConnectorInterface`, remove `Connector` adapter
(#47 by @clue)
```php
// old (explicit connector functions as an adapter)
$connector = $client->createConnector();
$promise = $connector->create('google.com', 80);
// new (client can be used as connector right away)
$promise = $client->create('google.com', 80);
```
* Feature / BC break: Remove `createSecureConnector()`, use `SecureConnector` instead
(#47 by @clue)
```php
// old (tight coupling and hidden dependency)
$tls = $client->createSecureConnector();
$promise = $tls->create('google.com', 443);
// new (more explicit, loose coupling)
$tls = new React\SocketClient\SecureConnector($client, $loop);
$promise = $tls->create('google.com', 443);
```
* Feature / BC break: Remove `setResolveLocal()` and local DNS resolution and default to remote DNS resolution, use `DnsConnector` instead
(#44 by @clue)
```php
// old (implicitly defaults to true, can be disabled)
$client->setResolveLocal(false);
$tcp = $client->createConnector();
$promise = $tcp->create('google.com', 80);
// new (always disabled, can be re-enabled like this)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->createCached('8.8.8.8', $loop);
$tcp = new React\SocketClient\DnsConnector($client, $resolver);
$promise = $tcp->create('google.com', 80);
```
* Feature / BC break: Remove `setTimeout()`, use `TimeoutConnector` instead
(#45 by @clue)
```php
// old (timeout only applies to TCP/IP connection)
$client = new Client('127.0.0.1', …);
$client->setTimeout(3.0);
$tcp = $client->createConnector();
$promise = $tcp->create('google.com', 80);
// new (timeout can be added to any layer)
$client = new Client('127.0.0.1', …);
$tcp = new React\SocketClient\TimeoutConnector($client, 3.0, $loop);
$promise = $tcp->create('google.com', 80);
```
* Feature / BC break: Remove `setProtocolVersion()` and `setAuth()` mutators, only support SOCKS URI for protocol version and authentication (immutable API)
(#46 by @clue)
```php
// old (state can be mutated after instantiation)
$client = new Client('127.0.0.1', …);
$client->setProtocolVersion('5');
$client->setAuth('user', 'pass');
// new (immutable after construction, already supported as of v0.5.2 - now mandatory)
$client = new Client('socks5://user:pass@127.0.0.1', …);
```
## 0.5.2 (2016-11-25)
* Feature: Apply protocol version and username/password auth from SOCKS URI
(#43 by @clue)
```php
// explicitly use SOCKS5
$client = new Client('socks5://127.0.0.1', $loop);
// use authentication (automatically SOCKS5)
$client = new Client('user:pass@127.0.0.1', $loop);
```
* More explicit client examples, including proxy chaining
(#42 by @clue)
## 0.5.1 (2016-11-21)
* Feature: Support Promise cancellation
(#39 by @clue)
```php
$promise = $connector->create($host, $port);
$promise->cancel();
```
* Feature: Timeout now cancels pending connection attempt
(#39, #22 by @clue)
## 0.5.0 (2016-11-07)
* Remove / BC break: Split off Server to clue/socks-server
(#35 by @clue)
> Upgrading? Check [clue/socks-server](https://github.com/clue/php-socks-server) for details.
* Improve documentation and project structure
## 0.4.0 (2016-03-19)
* Feature: Support proper SSL/TLS connections with additional SSL context options
(#31, #33 by @clue)
* Documentation for advanced Connector setups (bindto, multihop)
(#32 by @clue)
## 0.3.0 (2015-06-20)
* BC break / Feature: Client ctor now accepts a SOCKS server URI
([#24](https://github.com/clue/php-socks-react/pull/24))
```php
// old
$client = new Client($loop, 'localhost', 9050);
// new
$client = new Client('localhost:9050', $loop);
```
* Feature: Automatically assume default SOCKS port (1080) if not given explicitly
([#26](https://github.com/clue/php-socks-react/pull/26))
* Improve documentation and test suite
## 0.2.1 (2014-11-13)
* Support React PHP v0.4 (while preserving BC with React PHP v0.3)
([#16](https://github.com/clue/php-socks-react/pull/16))
* Improve examples and add first class support for HHVM
([#15](https://github.com/clue/php-socks-react/pull/15) and [#17](https://github.com/clue/php-socks-react/pull/17))
## 0.2.0 (2014-09-27)
* BC break / Feature: Simplify constructors by making parameters optional.
([#10](https://github.com/clue/php-socks-react/pull/10))
The `Factory` has been removed, you can now create instances of the `Client`
and `Server` yourself:
```php
// old
$factory = new Factory($loop, $dns);
$client = $factory->createClient('localhost', 9050);
$server = $factory->createSever($socket);
// new
$client = new Client($loop, 'localhost', 9050);
$server = new Server($loop, $socket);
```
* BC break: Remove HTTP support and link to [clue/buzz-react](https://github.com/clue/php-buzz-react) instead.
([#9](https://github.com/clue/php-socks-react/pull/9))
HTTP operates on a different layer than this low-level SOCKS library.
Removing this reduces the footprint of this library.
> Upgrading? Check the [README](https://github.com/clue/php-socks-react#http-requests) for details.
* Fix: Refactored to support other, faster loops (libev/libevent)
([#12](https://github.com/clue/php-socks-react/pull/12))
* Explicitly list dependencies, clean up examples and extend test suite significantly
## 0.1.0 (2014-05-19)
* First stable release
* Async SOCKS `Client` and `Server` implementation
* Project was originally part of [clue/socks](https://github.com/clue/php-socks)
and was split off from its latest releave v0.4.0
([#1](https://github.com/clue/reactphp-socks/issues/1))
> Upgrading from clue/socks v0.4.0? Use namespace `Clue\React\Socks` instead of `Socks` and you're ready to go!
## 0.0.0 (2011-04-26)
* Initial concept, originally tracked as part of
[clue/socks](https://github.com/clue/php-socks)

21
vendor/clue/socks-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2011 Christian Lück
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.

1116
vendor/clue/socks-react/README.md vendored Normal file

File diff suppressed because it is too large Load Diff

31
vendor/clue/socks-react/composer.json vendored Normal file
View File

@ -0,0 +1,31 @@
{
"name": "clue/socks-react",
"description": "Async SOCKS proxy connector client and server implementation, tunnel any TCP/IP-based protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of ReactPHP.",
"keywords": ["socks client", "socks server", "socks5", "socks4a", "proxy server", "tcp tunnel", "async", "ReactPHP"],
"homepage": "https://github.com/clue/reactphp-socks",
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"email": "christian@clue.engineering"
}
],
"require": {
"php": ">=5.3",
"react/promise": "^3 || ^2.1 || ^1.2",
"react/socket": "^1.12"
},
"require-dev": {
"clue/block-react": "^1.5",
"clue/connection-manager-extra": "^1.3",
"phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35",
"react/event-loop": "^1.2",
"react/http": "^1.6"
},
"autoload": {
"psr-4": { "Clue\\React\\Socks\\": "src/" }
},
"autoload-dev": {
"psr-4": { "Clue\\Tests\\React\\Socks\\": "tests/" }
}
}

458
vendor/clue/socks-react/src/Client.php vendored Normal file
View File

@ -0,0 +1,458 @@
<?php
namespace Clue\React\Socks;
use React\Promise;
use React\Promise\PromiseInterface;
use React\Promise\Deferred;
use React\Socket\ConnectionInterface;
use React\Socket\Connector;
use React\Socket\ConnectorInterface;
use React\Socket\FixedUriConnector;
use Exception;
use InvalidArgumentException;
use RuntimeException;
final class Client implements ConnectorInterface
{
/**
* @var ConnectorInterface
*/
private $connector;
private $socksUri;
private $protocolVersion = 5;
private $auth = null;
/**
* @param string $socksUri
* @param ?ConnectorInterface $connector
* @throws InvalidArgumentException
*/
public function __construct(
#[\SensitiveParameter]
$socksUri,
ConnectorInterface $connector = null
) {
// support `sockss://` scheme for SOCKS over TLS
// support `socks+unix://` scheme for Unix domain socket (UDS) paths
if (preg_match('/^(socks(?:5|4)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) {
// rewrite URI to parse SOCKS scheme, authentication and dummy host
$socksUri = $match[1] . '://' . $match[3] . 'localhost';
// connector uses appropriate transport scheme and explicit host given
$connector = new FixedUriConnector(
($match[2] === 's' ? 'tls://' : 'unix://') . $match[4],
$connector ?: new Connector()
);
}
// assume default scheme if none is given
if (strpos($socksUri, '://') === false) {
$socksUri = 'socks://' . $socksUri;
}
// parse URI into individual parts
$parts = parse_url($socksUri);
if (!$parts || !isset($parts['scheme'], $parts['host'])) {
throw new InvalidArgumentException('Invalid SOCKS server URI "' . $socksUri . '"');
}
// assume default port
if (!isset($parts['port'])) {
$parts['port'] = 1080;
}
// user or password in URI => SOCKS5 authentication
if (isset($parts['user']) || isset($parts['pass'])) {
if ($parts['scheme'] !== 'socks' && $parts['scheme'] !== 'socks5') {
// fail if any other protocol version given explicitly
throw new InvalidArgumentException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication');
}
$parts += array('user' => '', 'pass' => '');
$this->setAuth(rawurldecode($parts['user']), rawurldecode($parts['pass']));
}
// check for valid protocol version from URI scheme
$this->setProtocolVersionFromScheme($parts['scheme']);
$this->socksUri = $parts['host'] . ':' . $parts['port'];
$this->connector = $connector ?: new Connector();
}
private function setProtocolVersionFromScheme($scheme)
{
if ($scheme === 'socks' || $scheme === 'socks5') {
$this->protocolVersion = 5;
} elseif ($scheme === 'socks4') {
$this->protocolVersion = 4;
} else {
throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"');
}
}
/**
* set login data for username/password authentication method (RFC1929)
*
* @param string $username
* @param string $password
* @link http://tools.ietf.org/html/rfc1929
*/
private function setAuth(
$username,
#[\SensitiveParameter]
$password
) {
if (strlen($username) > 255 || strlen($password) > 255) {
throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each');
}
$this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password;
}
/**
* Establish a TCP/IP connection to the given target URI through the SOCKS server
*
* Many higher-level networking protocols build on top of TCP. It you're dealing
* with one such client implementation, it probably uses/accepts an instance
* implementing ReactPHP's `ConnectorInterface` (and usually its default `Connector`
* instance). In this case you can also pass this `Connector` instance instead
* to make this client implementation SOCKS-aware. That's it.
*
* @param string $uri
* @return PromiseInterface Promise<ConnectionInterface,Exception>
*/
public function connect($uri)
{
if (strpos($uri, '://') === false) {
$uri = 'tcp://' . $uri;
}
$parts = parse_url($uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
return Promise\reject(new InvalidArgumentException('Invalid target URI specified'));
}
$host = trim($parts['host'], '[]');
$port = $parts['port'];
if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) {
return Promise\reject(new InvalidArgumentException('Invalid target specified'));
}
// construct URI to SOCKS server to connect to
$socksUri = $this->socksUri;
// append path from URI if given
if (isset($parts['path'])) {
$socksUri .= $parts['path'];
}
// parse query args
$args = array();
if (isset($parts['query'])) {
parse_str($parts['query'], $args);
}
// append hostname from URI to query string unless explicitly given
if (!isset($args['hostname'])) {
$args['hostname'] = $host;
}
// append query string
$socksUri .= '?' . http_build_query($args, '', '&');
// append fragment from URI if given
if (isset($parts['fragment'])) {
$socksUri .= '#' . $parts['fragment'];
}
// start TCP/IP connection to SOCKS server
$connecting = $this->connector->connect($socksUri);
$deferred = new Deferred(function ($_, $reject) use ($uri, $connecting) {
$reject(new RuntimeException(
'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)',
defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103
));
// either close active connection or cancel pending connection attempt
$connecting->then(function (ConnectionInterface $stream) {
$stream->close();
});
$connecting->cancel();
});
// handle SOCKS protocol once connection is ready
// resolve plain connection once SOCKS protocol is completed
$that = $this;
$connecting->then(
function (ConnectionInterface $stream) use ($that, $host, $port, $deferred, $uri) {
$that->handleConnectedSocks($stream, $host, $port, $deferred, $uri);
},
function (Exception $e) use ($uri, $deferred) {
$deferred->reject($e = new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111,
$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);
}
);
return $deferred->promise();
}
/**
* Internal helper used to handle the communication with the SOCKS server
*
* @param ConnectionInterface $stream
* @param string $host
* @param int $port
* @param Deferred $deferred
* @param string $uri
* @return void
* @internal
*/
public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred, $uri)
{
$reader = new StreamReader();
$stream->on('data', array($reader, 'write'));
$stream->on('error', $onError = function (Exception $e) use ($deferred, $uri) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)',
defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e)
);
});
$stream->on('close', $onClose = function () use ($deferred, $uri) {
$deferred->reject(new RuntimeException(
'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response from proxy (ECONNRESET)',
defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104)
);
});
if ($this->protocolVersion === 5) {
$promise = $this->handleSocks5($stream, $host, $port, $reader, $uri);
} else {
$promise = $this->handleSocks4($stream, $host, $port, $reader, $uri);
}
$promise->then(function () use ($deferred, $stream, $reader, $onError, $onClose) {
$stream->removeListener('data', array($reader, 'write'));
$stream->removeListener('error', $onError);
$stream->removeListener('close', $onClose);
$deferred->resolve($stream);
}, function (Exception $error) use ($deferred, $stream, $uri) {
// pass custom RuntimeException through as-is, otherwise wrap in protocol error
if (!$error instanceof RuntimeException) {
$error = new RuntimeException(
'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)',
defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71,
$error
);
}
$deferred->reject($error);
$stream->close();
});
}
private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
{
// do not resolve hostname. only try to convert to IP
$ip = ip2long($host);
// send IP or (0.0.0.1) if invalid
$data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00);
if ($ip === false) {
// host is not a valid IP => send along hostname (SOCKS4a)
$data .= $host . pack('C', 0x00);
}
$stream->write($data);
return $reader->readBinary(array(
'null' => 'C',
'status' => 'C',
'port' => 'n',
'ip' => 'N'
))->then(function ($data) use ($uri) {
if ($data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x5a) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy refused connection with error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
}
});
}
private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri)
{
// protocol version 5
$data = pack('C', 0x05);
$auth = $this->auth;
if ($auth === null) {
// one method, no authentication
$data .= pack('C2', 0x01, 0x00);
} else {
// two methods, username/password and no authentication
$data .= pack('C3', 0x02, 0x02, 0x00);
}
$stream->write($data);
$that = $this;
return $reader->readBinary(array(
'version' => 'C',
'method' => 'C'
))->then(function ($data) use ($auth, $stream, $reader, $uri) {
if ($data['version'] !== 0x05) {
throw new Exception('Version/Protocol mismatch');
}
if ($data['method'] === 0x02 && $auth !== null) {
// username/password authentication requested and provided
$stream->write($auth);
return $reader->readBinary(array(
'version' => 'C',
'status' => 'C'
))->then(function ($data) use ($uri) {
if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access with given authentication details (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
);
}
});
} else if ($data['method'] !== 0x00) {
// any other method than "no authentication"
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access due to unsupported authentication method (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
);
}
})->then(function () use ($stream, $reader, $host, $port) {
// do not resolve hostname. only try to convert to (binary/packed) IP
$ip = @inet_pton($host);
$data = pack('C3', 0x05, 0x01, 0x00);
if ($ip === false) {
// not an IP, send as hostname
$data .= pack('C2', 0x03, strlen($host)) . $host;
} else {
// send as IPv4 / IPv6
$data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip;
}
$data .= pack('n', $port);
$stream->write($data);
return $reader->readBinary(array(
'version' => 'C',
'status' => 'C',
'null' => 'C',
'type' => 'C'
));
})->then(function ($data) use ($reader, $uri) {
if ($data['version'] !== 0x05 || $data['null'] !== 0x00) {
throw new Exception('Invalid SOCKS response');
}
if ($data['status'] !== 0x00) {
// map limited list of SOCKS error codes to common socket error conditions
// @link https://tools.ietf.org/html/rfc1928#section-6
if ($data['status'] === Server::ERROR_GENERAL) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy refused connection with general server failure (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
} elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy denied access due to ruleset (EACCES)',
defined('SOCKET_EACCES') ? SOCKET_EACCES : 13
);
} elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported network unreachable (ENETUNREACH)',
defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101
);
} elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported host unreachable (EHOSTUNREACH)',
defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113
);
} elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported connection refused (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
} elseif ($data['status'] === Server::ERROR_TTL) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy reported TTL/timeout expired (ETIMEDOUT)',
defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110
);
} elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy does not support the CONNECT command (EPROTO)',
defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
);
} elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) {
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy does not support this address type (EPROTO)',
defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71
);
}
throw new RuntimeException(
'Connection to ' . $uri . ' failed because proxy server refused connection with unknown error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)',
defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111
);
}
if ($data['type'] === 0x01) {
// IPv4 address => skip IP and port
return $reader->readLength(6);
} elseif ($data['type'] === 0x03) {
// domain name => read domain name length
return $reader->readBinary(array(
'length' => 'C'
))->then(function ($data) use ($reader) {
// skip domain name and port
return $reader->readLength($data['length'] + 2);
});
} elseif ($data['type'] === 0x04) {
// IPv6 address => skip IP and port
return $reader->readLength(18);
} else {
throw new Exception('Invalid SOCKS reponse: Invalid address type');
}
});
}
}

416
vendor/clue/socks-react/src/Server.php vendored Normal file
View File

@ -0,0 +1,416 @@
<?php
namespace Clue\React\Socks;
use React\Socket\ServerInterface;
use React\Promise\PromiseInterface;
use React\Socket\ConnectorInterface;
use React\Socket\Connector;
use React\Socket\ConnectionInterface;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use \UnexpectedValueException;
use \InvalidArgumentException;
use \Exception;
use React\Promise\Timer\TimeoutException;
final class Server
{
// the following error codes are only used for SOCKS5 only
/** @internal */
const ERROR_GENERAL = 0x01;
/** @internal */
const ERROR_NOT_ALLOWED_BY_RULESET = 0x02;
/** @internal */
const ERROR_NETWORK_UNREACHABLE = 0x03;
/** @internal */
const ERROR_HOST_UNREACHABLE = 0x04;
/** @internal */
const ERROR_CONNECTION_REFUSED = 0x05;
/** @internal */
const ERROR_TTL = 0x06;
/** @internal */
const ERROR_COMMAND_UNSUPPORTED = 0x07;
/** @internal */
const ERROR_ADDRESS_UNSUPPORTED = 0x08;
/** @var LoopInterface */
private $loop;
/** @var ConnectorInterface */
private $connector;
/**
* @var null|callable
*/
private $auth;
/**
*
* 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 ?LoopInterface $loop
* @param ?ConnectorInterface $connector
* @param null|array|callable $auth
*/
public function __construct(
LoopInterface $loop = null,
ConnectorInterface $connector = null,
#[\SensitiveParameter]
$auth = null
) {
if (\is_array($auth)) {
// wrap authentication array in authentication callback
$this->auth = function (
$username,
#[\SensitiveParameter]
$password
) use ($auth) {
return \React\Promise\resolve(
isset($auth[$username]) && (string)$auth[$username] === $password
);
};
} elseif (\is_callable($auth)) {
// wrap authentication callback in order to cast its return value to a promise
$this->auth = function(
$username,
#[\SensitiveParameter]
$password,
#[\SensitiveParameter]
$remote
) use ($auth) {
return \React\Promise\resolve(
\call_user_func($auth, $username, $password, $remote)
);
};
} elseif ($auth !== null) {
throw new \InvalidArgumentException('Invalid authenticator given');
}
$this->loop = $loop ?: Loop::get();
$this->connector = $connector ?: new Connector(array(), $this->loop);
}
/**
* @param ServerInterface $socket
* @return void
*/
public function listen(ServerInterface $socket)
{
$that = $this;
$socket->on('connection', function ($connection) use ($that) {
$that->onConnection($connection);
});
}
/** @internal */
public function onConnection(ConnectionInterface $connection)
{
$that = $this;
$handling = $this->handleSocks($connection)->then(null, function () use ($connection, $that) {
// SOCKS failed => close connection
$that->endConnection($connection);
});
$connection->on('close', function () use ($handling) {
$handling->cancel();
});
}
/**
* [internal] gracefully shutdown connection by flushing all remaining data and closing stream
*
* @internal
*/
public function endConnection(ConnectionInterface $stream)
{
$tid = true;
$loop = $this->loop;
// cancel below timer in case connection is closed in time
$stream->once('close', function () use (&$tid, $loop) {
// close event called before the timer was set up, so everything is okay
if ($tid === true) {
// make sure to not start a useless timer
$tid = false;
} else {
$loop->cancelTimer($tid);
}
});
// shut down connection by pausing input data, flushing outgoing buffer and then exit
$stream->pause();
$stream->end();
// check if connection is not already closed
if ($tid === true) {
// fall back to forcefully close connection in 3 seconds if buffer can not be flushed
$tid = $loop->addTimer(3.0, array($stream,'close'));
}
}
private function handleSocks(ConnectionInterface $stream)
{
$reader = new StreamReader();
$stream->on('data', array($reader, 'write'));
$that = $this;
$auth = $this->auth;
return $reader->readByte()->then(function ($version) use ($stream, $that, $auth, $reader){
if ($version === 0x04) {
if ($auth !== null) {
throw new UnexpectedValueException('SOCKS4 not allowed because authentication is required');
}
return $that->handleSocks4($stream, $reader);
} else if ($version === 0x05) {
return $that->handleSocks5($stream, $auth, $reader);
}
throw new UnexpectedValueException('Unexpected/unknown version number');
});
}
/** @internal */
public function handleSocks4(ConnectionInterface $stream, StreamReader $reader)
{
$remote = $stream->getRemoteAddress();
if ($remote !== null) {
// remove transport scheme and prefix socks4:// instead
$secure = strpos($remote, 'tls://') === 0;
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks4' . ($secure ? 's' : '') . '://' . $remote;
}
$that = $this;
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
return $reader->readBinary(array(
'port' => 'n',
'ipLong' => 'N',
'null' => 'C'
));
})->then(function ($data) use ($reader, $remote) {
if ($data['null'] !== 0x00) {
throw new Exception('Not a null byte');
}
if ($data['ipLong'] === 0) {
throw new Exception('Invalid IP');
}
if ($data['port'] === 0) {
throw new Exception('Invalid port');
}
if ($data['ipLong'] < 256) {
// invalid IP => probably a SOCKS4a request which appends the hostname
return $reader->readStringNull()->then(function ($string) use ($data, $remote){
return array($string, $data['port'], $remote);
});
} else {
$ip = long2ip($data['ipLong']);
return array($ip, $data['port'], $remote);
}
})->then(function ($target) use ($stream, $that) {
return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){
$stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
return $remote;
}, function($error) use ($stream){
$stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
throw $error;
});
}, function($error) {
throw new UnexpectedValueException('SOCKS4 protocol error',0,$error);
});
}
/** @internal */
public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader)
{
$remote = $stream->getRemoteAddress();
if ($remote !== null) {
// remove transport scheme and prefix socks5:// instead
$secure = strpos($remote, 'tls://') === 0;
if (($pos = strpos($remote, '://')) !== false) {
$remote = substr($remote, $pos + 3);
}
$remote = 'socks' . ($secure ? 's' : '') . '://' . $remote;
}
$that = $this;
return $reader->readByte()->then(function ($num) use ($reader) {
// $num different authentication mechanisms offered
return $reader->readLength($num);
})->then(function ($methods) use ($reader, $stream, $auth, &$remote) {
if ($auth === null && strpos($methods,"\x00") !== false) {
// accept "no authentication"
$stream->write(pack('C2', 0x05, 0x00));
return 0x00;
} else if ($auth !== null && strpos($methods,"\x02") !== false) {
// username/password authentication (RFC 1929) sub negotiation
$stream->write(pack('C2', 0x05, 0x02));
return $reader->readByteAssert(0x01)->then(function () use ($reader) {
return $reader->readByte();
})->then(function ($length) use ($reader) {
return $reader->readLength($length);
})->then(function ($username) use ($reader, $auth, $stream, &$remote) {
return $reader->readByte()->then(function ($length) use ($reader) {
return $reader->readLength($length);
})->then(function (
#[\SensitiveParameter]
$password
) use ($username, $auth, $stream, &$remote) {
// username and password given => authenticate
// prefix username/password to remote URI
if ($remote !== null) {
$remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote);
}
return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) {
if ($authenticated) {
// accept auth
$stream->write(pack('C2', 0x01, 0x00));
} else {
// reject auth => send any code but 0x00
$stream->end(pack('C2', 0x01, 0xFF));
throw new UnexpectedValueException('Authentication denied');
}
}, function ($e) use ($stream) {
// reject failed authentication => send any code but 0x00
$stream->end(pack('C2', 0x01, 0xFF));
throw new UnexpectedValueException('Authentication error', 0, $e);
});
});
});
} else {
// reject all offered authentication methods
$stream->write(pack('C2', 0x05, 0xFF));
throw new UnexpectedValueException('No acceptable authentication mechanism found');
}
})->then(function ($method) use ($reader) {
return $reader->readBinary(array(
'version' => 'C',
'command' => 'C',
'null' => 'C',
'type' => 'C'
));
})->then(function ($data) use ($reader) {
if ($data['version'] !== 0x05) {
throw new UnexpectedValueException('Invalid SOCKS version');
}
if ($data['command'] !== 0x01) {
throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED);
}
// if ($data['null'] !== 0x00) {
// throw new UnexpectedValueException('Reserved byte has to be NULL');
// }
if ($data['type'] === 0x03) {
// target hostname string
return $reader->readByte()->then(function ($len) use ($reader) {
return $reader->readLength($len);
});
} else if ($data['type'] === 0x01) {
// target IPv4
return $reader->readLength(4)->then(function ($addr) {
return inet_ntop($addr);
});
} else if ($data['type'] === 0x04) {
// target IPv6
return $reader->readLength(16)->then(function ($addr) {
return inet_ntop($addr);
});
} else {
throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED);
}
})->then(function ($host) use ($reader, &$remote) {
return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) {
return array($host, $data['port'], $remote);
});
})->then(function ($target) use ($that, $stream) {
return $that->connectTarget($stream, $target);
}, function($error) use ($stream) {
throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error);
})->then(function (ConnectionInterface $remote) use ($stream) {
$stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0));
return $remote;
}, function(Exception $error) use ($stream){
$stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0));
throw $error;
});
}
/** @internal */
public function connectTarget(ConnectionInterface $stream, array $target)
{
$uri = $target[0];
if (strpos($uri, ':') !== false) {
$uri = '[' . $uri . ']';
}
$uri .= ':' . $target[1];
// validate URI so a string hostname can not pass excessive URI parts
$parts = parse_url('tcp://' . $uri);
if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) {
return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given'));
}
if (isset($target[2])) {
$uri .= '?source=' . rawurlencode($target[2]);
}
$that = $this;
$connecting = $this->connector->connect($uri);
$stream->on('close', function () use ($connecting) {
$connecting->cancel();
});
return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) {
$stream->pipe($remote, array('end'=>false));
$remote->pipe($stream, array('end'=>false));
// remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local
$remote->on('end', function() use ($stream, $that) {
$that->endConnection($stream);
});
// local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote
$stream->on('end', function() use ($remote, $that) {
$that->endConnection($remote);
});
// set bigger buffer size of 100k to improve performance
$stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024;
return $remote;
}, function(Exception $error) {
// default to general/unknown error
$code = Server::ERROR_GENERAL;
// map common socket error conditions to limited list of SOCKS error codes
if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) {
$code = Server::ERROR_NOT_ALLOWED_BY_RULESET;
} elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) {
$code = Server::ERROR_HOST_UNREACHABLE;
} elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) {
$code = Server::ERROR_NETWORK_UNREACHABLE;
} elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') {
// Socket component does not currently assign an error code for this, so we have to resort to checking the exception message
$code = Server::ERROR_CONNECTION_REFUSED;
} elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) {
// Socket component does not currently assign an error code for this, but we can rely on the TimeoutException
$code = Server::ERROR_TTL;
}
throw new UnexpectedValueException('Unable to connect to remote target', $code, $error);
});
}
}

View File

@ -0,0 +1,149 @@
<?php
namespace Clue\React\Socks;
use React\Promise\Deferred;
use \InvalidArgumentException;
use \UnexpectedValueException;
/**
* @internal
*/
final class StreamReader
{
const RET_DONE = true;
const RET_INCOMPLETE = null;
private $buffer = '';
private $queue = array();
public function write($data)
{
$this->buffer .= $data;
do {
$current = reset($this->queue);
if ($current === false) {
break;
}
/* @var $current Closure */
$ret = $current($this->buffer);
if ($ret === self::RET_INCOMPLETE) {
// current is incomplete, so wait for further data to arrive
break;
} else {
// current is done, remove from list and continue with next
array_shift($this->queue);
}
} while (true);
}
public function readBinary($structure)
{
$length = 0;
$unpack = '';
foreach ($structure as $name=>$format) {
if ($length !== 0) {
$unpack .= '/';
}
$unpack .= $format . $name;
if ($format === 'C') {
++$length;
} else if ($format === 'n') {
$length += 2;
} else if ($format === 'N') {
$length += 4;
} else {
throw new InvalidArgumentException('Invalid format given');
}
}
return $this->readLength($length)->then(function ($response) use ($unpack) {
return unpack($unpack, $response);
});
}
public function readLength($bytes)
{
$deferred = new Deferred();
$this->readBufferCallback(function (&$buffer) use ($bytes, $deferred) {
if (strlen($buffer) >= $bytes) {
$deferred->resolve((string)substr($buffer, 0, $bytes));
$buffer = (string)substr($buffer, $bytes);
return StreamReader::RET_DONE;
}
});
return $deferred->promise();
}
public function readByte()
{
return $this->readBinary(array(
'byte' => 'C'
))->then(function ($data) {
return $data['byte'];
});
}
public function readByteAssert($expect)
{
return $this->readByte()->then(function ($byte) use ($expect) {
if ($byte !== $expect) {
throw new UnexpectedValueException('Unexpected byte encountered');
}
return $byte;
});
}
public function readStringNull()
{
$deferred = new Deferred();
$string = '';
$that = $this;
$readOne = function () use (&$readOne, $that, $deferred, &$string) {
$that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) {
if ($byte === 0x00) {
$deferred->resolve($string);
} else {
$string .= chr($byte);
$readOne();
}
});
};
$readOne();
return $deferred->promise();
}
public function readBufferCallback(/* callable */ $callable)
{
if (!is_callable($callable)) {
throw new InvalidArgumentException('Given function must be callable');
}
if ($this->queue) {
$this->queue []= $callable;
} else {
$this->queue = array($callable);
if ($this->buffer !== '') {
// this is the first element in the queue and the buffer is filled => trigger write procedure
$this->write('');
}
}
}
public function getBuffer()
{
return $this->buffer;
}
}

281
vendor/clue/stdio-react/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,281 @@
# Changelog
## 2.6.0 (2022-03-18)
* Feature: Full support for PHP 8.1 release.
(#101 by @cdosoftei)
## 2.5.0 (2021-10-25)
* Feature: Simplify usage by supporting new default loop.
(#99 by @clue)
```php
// old (still supported)
$stdio = new Clue\React\Stdio\Stdio($loop);
// new (using default loop)
$stdio = new Clue\React\Stdio\Stdio();
```
* Improve code examples and documentation.
(#100 by @clue and #98 by @PaulRotmann)
* Use GitHub actions for continuous integration (CI).
(#97 by @SimonFrings)
## 2.4.0 (2020-11-20)
* Fix: Refactor executing functional tests without ext-readline.
(#95 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from export.
Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix.
(#93 and #94 by @SimonFrings)
## 2.3.0 (2019-08-28)
* Feature: Emit audible/visible BELL signal when using a disabled function.
(#86 and #87 by @clue)
By default, this project will emit an audible/visible BELL signal when the user
tries to execute an otherwise disabled function, such as using the
<kbd>left</kbd> or <kbd>backspace</kbd> keys when already at the beginning of the line.
* Deprecated: Deprecate `Readline` class and move all methods to `Stdio`.
(#84 by @clue)
```php
// deprecated:
$stdio->getReadline()->setPrompt('> ');
// recommended alternative:
$stdio->setPrompt('> ');
```
* Fix: Fix closing to emit final `close` event and clean up all listeners.
(#88 by @clue)
* Improve test suite to test against legacy PHP 5.3 through PHP 7.3 and support PHPUnit 7.
(#85 by @clue)
## 2.2.0 (2018-09-03)
* Feature / Fix: Accept CR as an alias for LF to support more platforms.
(#79 by @clue)
The <kbd>enter</kbd> key will usually end the line with a `\n` (LF)
character on most Unix platforms. Common terminals also accept the
<kbd>^M</kbd> (CR) key in place of the <kbd>^J</kbd> (LF) key.
By now allowing CR as an alias for LF in this library, we can significantly
improve compatibility with this common usage pattern and improve platform
support. In particular, some platforms use different TTY settings (`icrnl`,
`igncr` and family) and depending on these settings emit different EOL
characters. This fixes issues where <kbd>enter</kbd> was not properly
detected when using `ext-readline` on Mac OS X, Android and others.
* Fix: Fix and simplify restoring TTY mode when `ext-readline` is not in use.
(#74 and #78 by @clue)
* Update project homepage, minor code style improvements and sort dependencies.
(#72 and #81 by @clue and #75 by @localheinz)
## 2.1.0 (2018-02-05)
* Feature: Add support for binding custom functions to any key code.
(#70 by @clue)
```php
$readline->on('?', function () use ($stdio) {
$stdio->write('Do you need help?');
});
```
* Feature: Add `addInput()` helper method.
(#69 by @clue)
```php
$readline->addInput('hello');
$readline->addInput(' world');
```
## 2.0.0 (2018-01-24)
A major compatibility release to update this package to support all latest
ReactPHP components!
This update involves a minor BC break due to dropped support for legacy
versions. 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: Remove all deprecated APIs (individual `Stdin`, `Stdout`, `line` etc.)
(#64 and #68 by @clue)
> All of this affects only what is considered "advanced usage".
If you're affected by this BC break, then it's recommended to first
update to the intermediary v1.2.0 release, which provides alternatives
to all deprecated APIs and then update to this version without causing a
BC break.
* Feature / BC break: Consistently emit incoming "data" event with trailing newline
unless stream ends without trailing newline (such as when piping).
(#65 by @clue)
* Feature: Forward compatibility with upcoming Stream v1.0 and EventLoop v1.0
and avoid blocking when `STDOUT` buffer is full.
(#68 by @clue)
## 1.2.0 (2017-12-18)
* Feature: Optionally use `ext-readline` to enable raw input mode if available.
This extension is entirely optional, but it is more efficient and reliable
than spawning the external `stty` command.
(#63 by @clue)
* Feature: Consistently return boolean success from `write()` and
avoid sending unneeded control codes between writes.
(#60 by @clue)
* Deprecated: Deprecate input helpers and output helpers and
recommend using `Stdio` as a normal `DuplexStreamInterface` instead.
(#59 and #62 by @clue)
```php
// deprecated
$stdio->on('line', function ($line) use ($stdio) {
$stdio->writeln("input: $line");
});
// recommended alternative
$stdio->on('data', function ($line) use ($stdio) {
$stdio->write("input: $line");
});
```
* Improve test suite by adding forward compatibility with PHPUnit 6.
(#61 by @carusogabriel)
## 1.1.0 (2017-11-01)
* Feature: Explicitly end stream on CTRL+D and emit final data on EOF without EOL.
(#56 by @clue)
* Feature: Support running on non-TTY and closing STDIN and STDOUT streams.
(#57 by @clue)
* Feature / Fix: Restore blocking mode before closing and restore TTY mode on unclean shutdown.
(#58 by @clue)
* Improve documentation to detail why async console I/O is not supported on Microsoft Windows.
(#54 by @clue)
* Improve test suite by adding PHPUnit to require-dev,
fix HHVM build for now again and ignore future HHVM build errors and
lock Travis distro so future defaults will not break the build.
(#46, #48 and #52 by @clue)
## 1.0.0 (2017-01-08)
* First stable release, now following SemVer.
> Contains no other changes, so it's actually fully compatible with the v0.5.0 release.
## 0.5.0 (2017-01-08)
* Feature: Add history support.
(#40 by @clue)
* Feature: Add autocomplete support.
(#41 by @clue)
* Feature: Suggest using ext-mbstring, otherwise use regex fallback.
(#42 by @clue)
* Remove / BC break: Remove undocumented and low quality skeletons/helpers for
`Buffer`, `ProgressBar` and `Spinner` (mostly dead code).
(#39, #43 by @clue)
* First class support for PHP 5.3 through PHP 7 and HHVM.
(#44 by @clue)
* Simplify and restructure examples.
(#45 by @clue)
## 0.4.0 (2016-09-27)
* Feature / BC break: The `Stdio` is now a well-behaving duplex stream.
(#35 by @clue)
* Feature / BC break: The `Readline` is now a well-behaving readable stream.
(#32 by @clue)
* Feature: Add `Readline::getPrompt()` helper.
(#33 by @clue)
* Feature / Fix: All unsupported special keys, key codes and byte sequences will now be ignored.
(#36, #30, #19, #38 by @clue)
* Fix: Explicitly redraw prompt on empty input.
(#37 by @clue)
* Fix: Writing data that contains multiple newlines will no longer end up garbled.
(#34, #35 by @clue)
## 0.3.1 (2015-11-26)
* Fix: Support calling `Readline::setInput()` during `line` event.
(#28)
```php
$stdio->on('line', function ($line) use ($stdio) {
$stdio->getReadline()->setInput($line . '!');
});
```
## 0.3.0 (2015-05-18)
* Feature: Support multi-byte UTF-8 characters and account for cell width.
(#20)
* Feature: Add support for HOME and END keys.
(#22)
* Fix: Clear readline input and restore TTY on end.
(#21)
## 0.2.0 (2015-05-17)
* Feature: Support echo replacements (asterisk for password prompts).
(#11)
```php
$stdio->getReadline()->setEcho('*');
```
* Feature: Add accessors for text input buffer and current cursor position.
(#8 and #9)
```php
$stdio->getReadline()->setInput('hello');
$stdio->getReadline()->getCursorPosition();
```
* Feature: All setters now return self to allow easy method chaining.
(#7)
```php
$stdio->getReadline()->setPrompt('Password: ')->setEcho('*')->setInput('secret');
```
* Feature: Only redraw() readline when necessary.
(#10 and #14)
## 0.1.0 (2014-09-08)
* First tagged release.
## 0.0.0 (2013-08-21)
* Initial concept.

21
vendor/clue/stdio-react/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Christian Lück
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.

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