diff options
Diffstat (limited to 'vendor/clue')
111 files changed, 16417 insertions, 0 deletions
diff --git a/vendor/clue/block-react/CHANGELOG.md b/vendor/clue/block-react/CHANGELOG.md new file mode 100644 index 0000000..5f145e9 --- /dev/null +++ b/vendor/clue/block-react/CHANGELOG.md @@ -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 diff --git a/vendor/clue/block-react/LICENSE b/vendor/clue/block-react/LICENSE new file mode 100644 index 0000000..dc09d1e --- /dev/null +++ b/vendor/clue/block-react/LICENSE @@ -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. diff --git a/vendor/clue/block-react/README.md b/vendor/clue/block-react/README.md new file mode 100644 index 0000000..d000a16 --- /dev/null +++ b/vendor/clue/block-react/README.md @@ -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. diff --git a/vendor/clue/block-react/composer.json b/vendor/clue/block-react/composer.json new file mode 100644 index 0000000..ddfc6c8 --- /dev/null +++ b/vendor/clue/block-react/composer.json @@ -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" + } +} diff --git a/vendor/clue/block-react/src/functions.php b/vendor/clue/block-react/src/functions.php new file mode 100644 index 0000000..6afe2e0 --- /dev/null +++ b/vendor/clue/block-react/src/functions.php @@ -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(); + } + } +} diff --git a/vendor/clue/block-react/src/functions_include.php b/vendor/clue/block-react/src/functions_include.php new file mode 100644 index 0000000..b3ad74c --- /dev/null +++ b/vendor/clue/block-react/src/functions_include.php @@ -0,0 +1,8 @@ +<?php + +namespace Clue\React\Block; + +if (!function_exists('Clue\\React\\Block\\sleep')) { + require __DIR__ . '/functions.php'; +} + diff --git a/vendor/clue/connection-manager-extra/CHANGELOG.md b/vendor/clue/connection-manager-extra/CHANGELOG.md new file mode 100644 index 0000000..5402fd5 --- /dev/null +++ b/vendor/clue/connection-manager-extra/CHANGELOG.md @@ -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 + diff --git a/vendor/clue/connection-manager-extra/LICENSE b/vendor/clue/connection-manager-extra/LICENSE new file mode 100644 index 0000000..da15612 --- /dev/null +++ b/vendor/clue/connection-manager-extra/LICENSE @@ -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. diff --git a/vendor/clue/connection-manager-extra/README.md b/vendor/clue/connection-manager-extra/README.md new file mode 100644 index 0000000..48eb599 --- /dev/null +++ b/vendor/clue/connection-manager-extra/README.md @@ -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. diff --git a/vendor/clue/connection-manager-extra/composer.json b/vendor/clue/connection-manager-extra/composer.json new file mode 100644 index 0000000..01075a9 --- /dev/null +++ b/vendor/clue/connection-manager-extra/composer.json @@ -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" + } +} diff --git a/vendor/clue/connection-manager-extra/src/ConnectionManagerDelay.php b/vendor/clue/connection-manager-extra/src/ConnectionManagerDelay.php new file mode 100644 index 0000000..a8a85c5 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerDelay.php @@ -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); + }); + } +} diff --git a/vendor/clue/connection-manager-extra/src/ConnectionManagerReject.php b/vendor/clue/connection-manager-extra/src/ConnectionManagerReject.php new file mode 100644 index 0000000..1222c83 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerReject.php @@ -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); + } +} diff --git a/vendor/clue/connection-manager-extra/src/ConnectionManagerRepeat.php b/vendor/clue/connection-manager-extra/src/ConnectionManagerRepeat.php new file mode 100644 index 0000000..5151610 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerRepeat.php @@ -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(); + } + }); + } +} diff --git a/vendor/clue/connection-manager-extra/src/ConnectionManagerSwappable.php b/vendor/clue/connection-manager-extra/src/ConnectionManagerSwappable.php new file mode 100644 index 0000000..d133225 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerSwappable.php @@ -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; + } +} diff --git a/vendor/clue/connection-manager-extra/src/ConnectionManagerTimeout.php b/vendor/clue/connection-manager-extra/src/ConnectionManagerTimeout.php new file mode 100644 index 0000000..5ec23a5 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerTimeout.php @@ -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; + }); + } +} diff --git a/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConcurrent.php b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConcurrent.php new file mode 100644 index 0000000..b6175f8 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConcurrent.php @@ -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; + }); + } +} diff --git a/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConsecutive.php b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConsecutive.php new file mode 100644 index 0000000..b3bfd4c --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConsecutive.php @@ -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(); + } + }); + } +} diff --git a/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerRandom.php b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerRandom.php new file mode 100644 index 0000000..88d1fd6 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerRandom.php @@ -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); + } +} diff --git a/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerSelective.php b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerSelective.php new file mode 100644 index 0000000..859ea90 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerSelective.php @@ -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; + } +} diff --git a/vendor/clue/http-proxy-react/CHANGELOG.md b/vendor/clue/http-proxy-react/CHANGELOG.md new file mode 100644 index 0000000..19d534d --- /dev/null +++ b/vendor/clue/http-proxy-react/CHANGELOG.md @@ -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 diff --git a/vendor/clue/http-proxy-react/LICENSE b/vendor/clue/http-proxy-react/LICENSE new file mode 100644 index 0000000..7baae8e --- /dev/null +++ b/vendor/clue/http-proxy-react/LICENSE @@ -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. diff --git a/vendor/clue/http-proxy-react/README.md b/vendor/clue/http-proxy-react/README.md new file mode 100644 index 0000000..b5337c1 --- /dev/null +++ b/vendor/clue/http-proxy-react/README.md @@ -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/). diff --git a/vendor/clue/http-proxy-react/composer.json b/vendor/clue/http-proxy-react/composer.json new file mode 100644 index 0000000..4941b1a --- /dev/null +++ b/vendor/clue/http-proxy-react/composer.json @@ -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/" } + } +} diff --git a/vendor/clue/http-proxy-react/src/ProxyConnector.php b/vendor/clue/http-proxy-react/src/ProxyConnector.php new file mode 100644 index 0000000..165e8ba --- /dev/null +++ b/vendor/clue/http-proxy-react/src/ProxyConnector.php @@ -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(); + } +} diff --git a/vendor/clue/mq-react/CHANGELOG.md b/vendor/clue/mq-react/CHANGELOG.md new file mode 100644 index 0000000..8e491d4 --- /dev/null +++ b/vendor/clue/mq-react/CHANGELOG.md @@ -0,0 +1,96 @@ +# Changelog + +## 1.6.0 (2023-07-28) + +* Feature: Improve Promise v3 support and use template types. + (#41 and #42 by @clue) + +* Feature: Improve PHP 8.2+ support by refactoring queuing logic. + (#43 by @clue) + +* Improve test suite, ensure 100% code coverage and report failed assertions. + (#37 and #39 by @clue) + +## 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. diff --git a/vendor/clue/mq-react/LICENSE b/vendor/clue/mq-react/LICENSE new file mode 100644 index 0000000..984ff9d --- /dev/null +++ b/vendor/clue/mq-react/LICENSE @@ -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. diff --git a/vendor/clue/mq-react/README.md b/vendor/clue/mq-react/README.md new file mode 100644 index 0000000..10c0999 --- /dev/null +++ b/vendor/clue/mq-react/README.md @@ -0,0 +1,532 @@ +# clue/reactphp-mq + +[![CI status](https://github.com/clue/reactphp-mq/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-mq/actions) +[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests) +[![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(mixed):PromiseInterface<T> $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<TKey,TIn> $jobs, callable(TIn):PromiseInterface<TOut> $handler): PromiseInterface<array<TKey,TOut>>` 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<TKey,TIn> $jobs, callable(TIn):Promise<TOut> $handler): PromiseInterface<TOut>` 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.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+. +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 is set up to always ensure 100% code coverage across all +supported environments. If you have the Xdebug extension installed, you can also +generate a code coverage report locally like this: + +```bash +XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text +``` + +## 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. diff --git a/vendor/clue/mq-react/composer.json b/vendor/clue/mq-react/composer.json new file mode 100644 index 0000000..8a83e34 --- /dev/null +++ b/vendor/clue/mq-react/composer.json @@ -0,0 +1,33 @@ +{ + "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" + } + ], + "require": { + "php": ">=5.3", + "react/promise": "^3 || ^2.2.1 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4 || ^3 || ^2", + "react/event-loop": "^1.2", + "react/http": "^1.8" + }, + "autoload": { + "psr-4": { + "Clue\\React\\Mq\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Clue\\Tests\\React\\Mq\\": "tests/" + } + } +} diff --git a/vendor/clue/mq-react/src/Queue.php b/vendor/clue/mq-react/src/Queue.php new file mode 100644 index 0000000..2287514 --- /dev/null +++ b/vendor/clue/mq-react/src/Queue.php @@ -0,0 +1,465 @@ +<?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. + * + * @template T + */ +class Queue implements \Countable +{ + private $concurrency; + private $limit; + private $handler; + + /** @var int<0,max> */ + private $pending = 0; + + /** @var array<int,\Closure():void> */ + 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. + * + * @template TKey + * @template TIn + * @template TOut + * @param int $concurrency concurrency soft limit + * @param array<TKey,TIn> $jobs + * @param callable(TIn):PromiseInterface<TOut> $handler + * @return PromiseInterface<array<TKey,TOut>> Returns a Promise 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')); + * ``` + * + * @template TKey + * @template TIn + * @template TOut + * @param int $concurrency concurrency soft limit + * @param array<TKey,TIn> $jobs + * @param callable(TIn):PromiseInterface<TOut> $handler + * @return PromiseInterface<TOut> Returns a Promise 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(mixed):PromiseInterface<T> $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 PromiseInterface<T> + */ + 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); + assert(is_int($id)); + + /** @var ?PromiseInterface<T> $pending */ + $pending = null; + + $deferred = new Deferred(function ($_, $reject) use (&$queue, $id, &$pending) { + // forward cancellation to pending operation if it is currently executing + if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { + $pending->cancel(); + } + $pending = null; + + if (isset($queue[$id])) { + // queued promise cancelled before its handler is invoked + // remove from queue and reject explicitly + unset($queue[$id]); + $reject(new \RuntimeException('Cancelled queued job before processing started')); + } + }); + + // queue job to process if number of pending jobs is below concurrency limit again + $handler = $this->handler; // PHP 5.4+ + $args = func_get_args(); + $that = $this; // PHP 5.4+ + $queue[$id] = function () use ($handler, $args, $deferred, &$pending, $that) { + $pending = \call_user_func_array($handler, $args); + + $that->await($pending)->then( + function ($result) use ($deferred, &$pending) { + $pending = null; + $deferred->resolve($result); + }, + function ($e) use ($deferred, &$pending) { + $pending = null; + $deferred->reject($e); + } + ); + }; + + return $deferred->promise(); + } + + #[\ReturnTypeWillChange] + public function count() + { + return $this->pending + count($this->queue); + } + + /** + * @internal + * @param PromiseInterface<T> $promise + */ + public function await(PromiseInterface $promise) + { + $that = $this; // PHP 5.4+ + + 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; + } + + $next = reset($this->queue); + assert($next instanceof \Closure); + 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; + + // invoke handler and await its resolution before invoking next queued job + $next(); + } +} diff --git a/vendor/clue/redis-protocol/.travis.yml b/vendor/clue/redis-protocol/.travis.yml new file mode 100644 index 0000000..2af5cf5 --- /dev/null +++ b/vendor/clue/redis-protocol/.travis.yml @@ -0,0 +1,8 @@ +language: php +php: + - 5.4 + - 5.3 +before_script: + - composer install --dev --prefer-source --no-interaction +script: + - phpunit --coverage-text diff --git a/vendor/clue/redis-protocol/CHANGELOG.md b/vendor/clue/redis-protocol/CHANGELOG.md new file mode 100644 index 0000000..d037017 --- /dev/null +++ b/vendor/clue/redis-protocol/CHANGELOG.md @@ -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 + diff --git a/vendor/clue/redis-protocol/README.md b/vendor/clue/redis-protocol/README.md new file mode 100644 index 0000000..ea36a67 --- /dev/null +++ b/vendor/clue/redis-protocol/README.md @@ -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. diff --git a/vendor/clue/redis-protocol/composer.json b/vendor/clue/redis-protocol/composer.json new file mode 100644 index 0000000..d99e2ee --- /dev/null +++ b/vendor/clue/redis-protocol/composer.json @@ -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" } + } +} diff --git a/vendor/clue/redis-protocol/example/client.php b/vendor/clue/redis-protocol/example/client.php new file mode 100644 index 0000000..d1b4428 --- /dev/null +++ b/vendor/clue/redis-protocol/example/client.php @@ -0,0 +1,22 @@ +<?php + +require __DIR__ . '/../vendor/autoload.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" diff --git a/vendor/clue/redis-protocol/example/perf.php b/vendor/clue/redis-protocol/example/perf.php new file mode 100644 index 0000000..c5ce6a7 --- /dev/null +++ b/vendor/clue/redis-protocol/example/perf.php @@ -0,0 +1,31 @@ +<?php + +use Clue\Redis\Protocol\ProtocolBuffer; +use Clue\Redis\Protocol\Factory; + +require __DIR__ . '/../vendor/autoload.php'; + +$factory = new Factory(); +$parser = $factory->createResponseParser(); +$serializer = $factory->createSerializer(); + +$n = isset($argv[1]) ? (int)$argv[1] : 10000; // number of dummy messages to parse +$cs = 4096; // pretend we can only read 7 bytes at once. more like 4096/8192 usually + +echo 'benchmarking ' . $n . ' messages (chunksize of ' . $cs .' bytes)' . PHP_EOL; + +$time = microtime(true); + +$stream = ''; +for ($i = 0; $i < $n; ++$i) { + $stream .= $serializer->getRequestMessage('set', array('var' . $i, 'value' . $i)); +} + +echo round(microtime(true) - $time, 3) . 's for serialization' . PHP_EOL; +$time = microtime(true); + +for ($i = 0, $l = strlen($stream); $i < $l; $i += $cs) { + $parser->pushIncoming(substr($stream, $i, $cs)); +} + +echo round(microtime(true) - $time, 3) . 's for parsing' . PHP_EOL; diff --git a/vendor/clue/redis-protocol/phpunit.xml.dist b/vendor/clue/redis-protocol/phpunit.xml.dist new file mode 100644 index 0000000..4f3de2a --- /dev/null +++ b/vendor/clue/redis-protocol/phpunit.xml.dist @@ -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>
\ No newline at end of file diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Factory.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Factory.php new file mode 100644 index 0000000..3997f04 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Factory.php @@ -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(); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/BulkReply.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/BulkReply.php new file mode 100644 index 0000000..e069fda --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/BulkReply.php @@ -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); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/ErrorReply.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/ErrorReply.php new file mode 100644 index 0000000..556e93b --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/ErrorReply.php @@ -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()); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/IntegerReply.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/IntegerReply.php new file mode 100644 index 0000000..ba1ff05 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/IntegerReply.php @@ -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); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/ModelInterface.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/ModelInterface.php new file mode 100644 index 0000000..b97939e --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/ModelInterface.php @@ -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); +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/MultiBulkReply.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/MultiBulkReply.php new file mode 100644 index 0000000..7198dc6 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/MultiBulkReply.php @@ -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); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/Request.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/Request.php new file mode 100644 index 0000000..f5881e9 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/Request.php @@ -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); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/StatusReply.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/StatusReply.php new file mode 100644 index 0000000..4ea2fcd --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Model/StatusReply.php @@ -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); + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/MessageBuffer.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/MessageBuffer.php new file mode 100644 index 0000000..c1e3001 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/MessageBuffer.php @@ -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; + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserException.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserException.php new file mode 100644 index 0000000..e57c5bc --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserException.php @@ -0,0 +1,10 @@ +<?php + +namespace Clue\Redis\Protocol\Parser; + +use UnexpectedValueException; + +class ParserException extends UnexpectedValueException +{ + +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserInterface.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserInterface.php new file mode 100644 index 0000000..a322719 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ParserInterface.php @@ -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); +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/RequestParser.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/RequestParser.php new file mode 100644 index 0000000..a47d137 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/RequestParser.php @@ -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; + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ResponseParser.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ResponseParser.php new file mode 100644 index 0000000..19ac90c --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Parser/ResponseParser.php @@ -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; + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/RecursiveSerializer.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/RecursiveSerializer.php new file mode 100644 index 0000000..6e25125 --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/RecursiveSerializer.php @@ -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; + } +} diff --git a/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/SerializerInterface.php b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/SerializerInterface.php new file mode 100644 index 0000000..bb7cb3e --- /dev/null +++ b/vendor/clue/redis-protocol/src/Clue/Redis/Protocol/Serializer/SerializerInterface.php @@ -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); +} diff --git a/vendor/clue/redis-protocol/tests/FactoryTest.php b/vendor/clue/redis-protocol/tests/FactoryTest.php new file mode 100644 index 0000000..b669223 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/FactoryTest.php @@ -0,0 +1,34 @@ +<?php + +use Clue\Redis\Protocol\Factory; + +class FactoryTest extends TestCase +{ + private $factory; + + public function setUp() + { + $this->factory = new Factory(); + } + + public function testCreateResponseParser() + { + $parser = $this->factory->createResponseParser(); + + $this->assertInstanceOf('Clue\Redis\Protocol\Parser\ParserInterface', $parser); + } + + public function testCreateRequestParser() + { + $parser = $this->factory->createRequestParser(); + + $this->assertInstanceOf('Clue\Redis\Protocol\Parser\ParserInterface', $parser); + } + + public function testCreateSerializer() + { + $serializer = $this->factory->createSerializer(); + + $this->assertInstanceOf('Clue\Redis\Protocol\Serializer\SerializerInterface', $serializer); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/AbstractModelTest.php b/vendor/clue/redis-protocol/tests/Model/AbstractModelTest.php new file mode 100644 index 0000000..1358533 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/AbstractModelTest.php @@ -0,0 +1,22 @@ +<?php + +use Clue\Redis\Protocol\Serializer\RecursiveSerializer; + +abstract class AbstractModelTest extends TestCase +{ + protected $serializer; + + abstract protected function createModel($value); + + public function setUp() + { + $this->serializer = new RecursiveSerializer(); + } + + public function testConstructor() + { + $model = $this->createModel(null); + + $this->assertInstanceOf('Clue\Redis\Protocol\Model\ModelInterface', $model); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/BulkReplyTest.php b/vendor/clue/redis-protocol/tests/Model/BulkReplyTest.php new file mode 100644 index 0000000..78ed04c --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/BulkReplyTest.php @@ -0,0 +1,43 @@ +<?php + +use Clue\Redis\Protocol\Model\BulkReply; + +class BulkReplyTest extends AbstractModelTest +{ + protected function createModel($value) + { + return new BulkReply($value); + } + + public function testStringReply() + { + $model = $this->createModel('test'); + + $this->assertEquals('test', $model->getValueNative()); + $this->assertEquals("$4\r\ntest\r\n", $model->getMessageSerialized($this->serializer)); + } + + public function testEmptyStringReply() + { + $model = $this->createModel(''); + + $this->assertEquals('', $model->getValueNative()); + $this->assertEquals("$0\r\n\r\n", $model->getMessageSerialized($this->serializer)); + } + + public function testIntegerCast() + { + $model = $this->createModel(123); + + $this->assertEquals('123', $model->getValueNative()); + $this->assertEquals("$3\r\n123\r\n", $model->getMessageSerialized($this->serializer)); + } + + public function testNullBulkReply() + { + $model = $this->createModel(null); + + $this->assertEquals(null, $model->getValueNative()); + $this->assertEquals("$-1\r\n", $model->getMessageSerialized($this->serializer)); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/ErrorReplyTest.php b/vendor/clue/redis-protocol/tests/Model/ErrorReplyTest.php new file mode 100644 index 0000000..2585b08 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/ErrorReplyTest.php @@ -0,0 +1,19 @@ +<?php + +use Clue\Redis\Protocol\Model\ErrorReply; + +class ErrorReplyTest extends AbstractModelTest +{ + protected function createModel($value) + { + return new ErrorReply($value); + } + + public function testError() + { + $model = $this->createModel('ERR error'); + + $this->assertEquals('ERR error', $model->getValueNative()); + $this->assertEquals("-ERR error\r\n", $model->getMessageSerialized($this->serializer)); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/IntegerReplyTest.php b/vendor/clue/redis-protocol/tests/Model/IntegerReplyTest.php new file mode 100644 index 0000000..f6a2e10 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/IntegerReplyTest.php @@ -0,0 +1,40 @@ +<?php + +use Clue\Redis\Protocol\Model\IntegerReply; + +class IntegerReplyTest extends AbstractModelTest +{ + protected function createModel($value) + { + return new IntegerReply($value); + } + + public function testIntegerReply() + { + $model = $this->createModel(0); + $this->assertEquals(0, $model->getValueNative()); + $this->assertEquals(":0\r\n", $model->getMessageSerialized($this->serializer)); + } + + public function testFloatCasted() + { + $model = $this->createModel(-12.99); + $this->assertEquals(-12, $model->getValueNative()); + $this->assertEquals(":-12\r\n", $model->getMessageSerialized($this->serializer)); + + $model = $this->createModel(14.99); + $this->assertEquals(14, $model->getValueNative()); + $this->assertEquals(":14\r\n", $model->getMessageSerialized($this->serializer)); + } + + public function testBooleanCasted() + { + $model = $this->createModel(true); + $this->assertEquals(1, $model->getValueNative()); + $this->assertEquals(":1\r\n", $model->getMessageSerialized($this->serializer)); + + $model = $this->createModel(false); + $this->assertEquals(0, $model->getValueNative()); + $this->assertEquals(":0\r\n", $model->getMessageSerialized($this->serializer)); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/MultiBulkReplyTest.php b/vendor/clue/redis-protocol/tests/Model/MultiBulkReplyTest.php new file mode 100644 index 0000000..04dd389 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/MultiBulkReplyTest.php @@ -0,0 +1,115 @@ +<?php + +use Clue\Redis\Protocol\Model\MultiBulkReply; +use Clue\Redis\Protocol\Model\BulkReply; +use Clue\Redis\Protocol\Model\IntegerReply; + +class MultiBulkReplyTest extends AbstractModelTest +{ + protected function createModel($value) + { + return new MultiBulkReply($value); + } + + public function testEmptyArray() + { + $model = $this->createModel(array()); + + $this->assertEquals(array(), $model->getValueNative()); + $this->assertEquals("*0\r\n", $model->getMessageSerialized($this->serializer)); + + $this->assertFalse($model->isRequest()); + } + + public function testNullMultiBulkReply() + { + $model = $this->createModel(null); + + $this->assertEquals(null, $model->getValueNative()); + $this->assertEquals("*-1\r\n", $model->getMessageSerialized($this->serializer)); + + $this->assertFalse($model->isRequest()); + + return $model; + } + + /** + * @param MultiBulkReply $model + * @depends testNullMultiBulkReply + * @expectedException UnexpectedValueException + */ + public function testNullMultiBulkReplyIsNotARequest(MultiBulkReply $model) + { + $model->getRequestModel(); + } + + public function testSingleBulkEnclosed() + { + $model = $this->createModel(array(new BulkReply('test'))); + + $this->assertEquals(array('test'), $model->getValueNative()); + $this->assertEquals("*1\r\n$4\r\ntest\r\n", $model->getMessageSerialized($this->serializer)); + + $this->assertTrue($model->isRequest()); + + // this can be represented by a request + $request = $model->getRequestModel(); + $this->assertEquals($model->getValueNative(), $request->getValueNative()); + + // representing the request as a reply should return our original instance + $reply = $request->getReplyModel(); + $this->assertEquals($model, $reply); + + return $model; + } + + /** + * @depends testSingleBulkEnclosed + */ + public function testStringEnclosedEqualsSingleBulk(MultiBulkReply $expected) + { + $model = $this->createModel(array('test')); + + $this->assertEquals($expected->getValueNative(), $model->getValueNative()); + $this->assertEquals($expected->getMessageSerialized($this->serializer), $model->getMessageSerialized($this->serializer)); + + $this->assertTrue($model->isRequest()); + } + + public function testMixedReply() + { + $model = $this->createModel(array(new BulkReply('test'), new IntegerReply(123))); + + $this->assertEquals(array('test', 123), $model->getValueNative()); + $this->assertEquals("*2\r\n$4\r\ntest\r\n:123\r\n", $model->getMessageSerialized($this->serializer)); + + $this->assertFalse($model->isRequest()); + + return $model; + } + + /** + * @param MultiBulkReply $model + * @depends testMixedReply + * @expectedException UnexpectedValueException + */ + public function testMixedReplyIsNotARequest(MultiBulkReply $model) + { + $model->getRequestModel(); + } + + public function testMultiStrings() + { + $model = $this->createModel(array('SET', 'a', 'b')); + + $this->assertEquals(array('SET', 'a', 'b'), $model->getValueNative()); + + $this->assertTrue($model->isRequest()); + + $request = $model->getRequestModel(); + + // this can be represented by a request + $request = $model->getRequestModel(); + $this->assertEquals($model->getValueNative(), $request->getValueNative()); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/RequestTest.php b/vendor/clue/redis-protocol/tests/Model/RequestTest.php new file mode 100644 index 0000000..6719481 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/RequestTest.php @@ -0,0 +1,37 @@ +<?php + +use Clue\Redis\Protocol\Model\Request; + +class RequestTest extends AbstractModelTest +{ + protected function createModel($value) + { + return new Request('QUIT'); + } + + public function testPing() + { + $model = new Request('PING'); + + $this->assertEquals('PING', $model->getCommand()); + $this->assertEquals(array(), $model->getArgs()); + $this->assertEquals(array('PING'), $model->getValueNative()); + $this->assertEquals("*1\r\n$4\r\nPING\r\n", $model->getMessageSerialized($this->serializer)); + + $reply = $model->getReplyModel(); + $this->assertEquals($model->getValueNative(), $reply->getValueNative()); + } + + public function testGet() + { + $model = new Request('GET', array('a')); + + $this->assertEquals('GET', $model->getCommand()); + $this->assertEquals(array('a'), $model->getArgs()); + $this->assertEquals(array('GET', 'a'), $model->getValueNative()); + $this->assertEquals("*2\r\n$3\r\nGET\r\n$1\r\na\r\n", $model->getMessageSerialized($this->serializer)); + + $reply = $model->getReplyModel(); + $this->assertEquals($model->getValueNative(), $reply->getValueNative()); + } +} diff --git a/vendor/clue/redis-protocol/tests/Model/StatusReplyTest.php b/vendor/clue/redis-protocol/tests/Model/StatusReplyTest.php new file mode 100644 index 0000000..8debd85 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Model/StatusReplyTest.php @@ -0,0 +1,19 @@ +<?php + +use Clue\Redis\Protocol\Model\StatusReply; + +class StatusReplyTest extends AbstractModelTest +{ + protected function createModel($value) + { + return new StatusReply($value); + } + + public function testStatusOk() + { + $model = $this->createModel('OK'); + + $this->assertEquals('OK', $model->getValueNative()); + $this->assertEquals("+OK\r\n", $model->getMessageSerialized($this->serializer)); + } +} diff --git a/vendor/clue/redis-protocol/tests/Parser/AbstractParserTest.php b/vendor/clue/redis-protocol/tests/Parser/AbstractParserTest.php new file mode 100644 index 0000000..3d45c20 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Parser/AbstractParserTest.php @@ -0,0 +1,67 @@ +<?php + +use Clue\Redis\Protocol\Parser\ParserInterface; +use Clue\Redis\Protocol\Parser\MessageBuffer; + +abstract class AbstractParserTest extends TestCase +{ + /** + * + * @var ParserInterface + */ + protected $parser; + + abstract protected function createParser(); + + public function setUp() + { + $this->parser = $this->createParser(); + $this->assertInstanceOf('Clue\Redis\Protocol\Parser\ParserInterface', $this->parser); + } + + public function testParsingMessageOne() + { + // getRequestMessage('test') + $message = $expected = "*1\r\n$4\r\ntest\r\n"; + + $models = $this->parser->pushIncoming($message); + $this->assertCount(1, $models); + + $model = reset($models); + $this->assertEquals(array('test'), $model->getValueNative()); + } + + public function testParsingMessageTwoPartial() + { + // getRequestMessage('test', array('second')) + $message = "*2\r\n$4\r\ntest\r\n$6\r\nsecond\r\n"; + + $this->assertEquals(array(), $this->parser->pushIncoming(substr($message, 0, 1))); + $this->assertEquals(array(), $this->parser->pushIncoming(substr($message, 1, 1))); + $this->assertEquals(array(), $this->parser->pushIncoming(substr($message, 2, 1))); + $this->assertEquals(array(), $this->parser->pushIncoming(substr($message, 3, 10))); + $this->assertCount(1, $models = $this->parser->pushIncoming(substr($message, 13))); + + $model = reset($models); + + $this->assertEquals(array('test', 'second'), $model->getValueNative()); + } + + public function testMessageBuffer() + { + $buffer = new MessageBuffer($this->parser); + + $this->assertFalse($buffer->hasIncomingModel()); + + $data = "*1\r\n$4\r\ntest\r\n"; + $this->assertCount(1, $models = $buffer->pushIncoming($data)); + $this->assertTrue($buffer->hasIncomingModel()); + + $expected = reset($models); + $this->assertSame($expected, $buffer->popIncomingModel()); + $this->assertFalse($buffer->hasIncomingModel()); + + $this->setExpectedException('UnderflowException'); + $buffer->popIncomingModel(); + } +} diff --git a/vendor/clue/redis-protocol/tests/Parser/RequestParserTest.php b/vendor/clue/redis-protocol/tests/Parser/RequestParserTest.php new file mode 100644 index 0000000..645b673 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Parser/RequestParserTest.php @@ -0,0 +1,132 @@ +<?php + +use Clue\Redis\Protocol\Parser\RequestParser; + +class RequestParserTest extends AbstractParserTest +{ + protected function createParser() + { + return new RequestParser(); + } + + public function testSimplePingRequest() + { + $message = "*1\r\n$4\r\nping\r\n"; + + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $request = reset($models); + + $this->assertInstanceOf('Clue\Redis\Protocol\Model\Request', $request); + $this->assertEquals('ping', $request->getCommand()); + $this->assertEquals(array(), $request->getArgs()); + + return $request; + } + + /** + * + * @param Request $expected + * @depends testSimplePingRequest + */ + public function testInlinePingRequest($expected) + { + $message = "ping\r\n"; + + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $request = reset($models); + + $this->assertEquals($expected, $request); + } + + public function testInlineWhitespaceIsIgnored() + { + $message = " set name value \r\n"; + + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $request = reset($models); + + $this->assertInstanceOf('Clue\Redis\Protocol\Model\Request', $request); + $this->assertEquals('set', $request->getCommand()); + $this->assertEquals(array('name', 'value'), $request->getArgs()); + } + + public function testIncompleteSuccessive() + { + $this->assertEquals(array(), $this->parser->pushIncoming("*1\r\n")); + $this->assertEquals(array(), $this->parser->pushIncoming("$4\r\n")); + $this->assertEquals(array(), $this->parser->pushIncoming("test")); + $this->assertCount(1, $models = $this->parser->pushIncoming("\r\n")); + } + + public function testNullMultiBulkRequestIsIgnored() + { + $message = "*-1\r\n"; + + $this->assertEquals(array(), $this->parser->pushIncoming($message)); + } + + public function testEmptyMultiBulkRequestIsIgnored() + { + $message = "*0\r\n"; + + $this->assertEquals(array(), $this->parser->pushIncoming($message)); + } + + public function testEmptyInlineIsIgnored() + { + $message = "\r\n"; + + $this->assertEquals(array(), $this->parser->pushIncoming($message)); + } + + public function testInlineParsesMultipleRequestsAtOnce() + { + $message = "hello\r\n\world\r\ntest\r\n"; + + $this->assertCount(3, $models = $this->parser->pushIncoming($message)); + } + + + public function testEmptyInlineAroundInlineIsIgnored() + { + $message = "\r\n\r\n" . "ping\r\n\r\n"; + + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $request = reset($models); + + $this->assertInstanceOf('Clue\Redis\Protocol\Model\Request', $request); + $this->assertEquals('ping', $request->getCommand()); + $this->assertEquals(array(), $request->getArgs()); + } + + public function testWhitespaceInlineIsIgnored() + { + $message = " \r\n"; + + $this->assertEquals(array(), $this->parser->pushIncoming($message)); + } + + /** + * @expectedException Clue\Redis\Protocol\Parser\ParserException + */ + public function testInvalidMultiBulkMustContainBulk() + { + $message = "*1\r\n:123\r\n"; + + $this->parser->pushIncoming($message); + } + + /** + * @expectedException Clue\Redis\Protocol\Parser\ParserException + */ + public function testInvalidBulkLength() + { + $message = "*1\r\n$-1\r\n"; + + $this->parser->pushIncoming($message); + } +} diff --git a/vendor/clue/redis-protocol/tests/Parser/ResponseParserTest.php b/vendor/clue/redis-protocol/tests/Parser/ResponseParserTest.php new file mode 100644 index 0000000..762c8bd --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Parser/ResponseParserTest.php @@ -0,0 +1,130 @@ +<?php + +use Clue\Redis\Protocol\Parser\ResponseParser; + +class RecursiveParserTest extends AbstractParserTest +{ + protected function createParser() + { + return new ResponseParser(); + } + + public function testPartialIncompleteBulkReply() + { + $this->assertEquals(array(), $this->parser->pushIncoming("$20\r\nincompl")); + } + + public function testParsingStatusReplies() + { + // C: PING + $message = "+PONG\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals('PONG', $data); + + // C: SET key value + $message = "+OK\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals('OK', $data); + } + + public function testParsingErrorReply() + { + $message = "-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"; + + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + $exception = reset($models); + + $this->assertInstanceOf('Exception', $exception); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\ErrorReply', $exception); + $this->assertEquals('WRONGTYPE Operation against a key holding the wrong kind of value', $exception->getMessage()); + } + + public function testParsingIntegerReply() + { + // C: INCR mykey + $message = ":1\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals(1, $data); + } + + public function testParsingBulkReply() + { + // C: GET mykey + $message = "$6\r\nfoobar\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals("foobar", $data); + } + + public function testParsingNullBulkReply() + { + // C: GET nonexistingkey + $message = "$-1\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals(null, $data); + } + + public function testParsingEmptyMultiBulkReply() + { + // C: LRANGE nokey 0 1 + $message = "*0\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals(array(), $data); + } + + public function testParsingNullMultiBulkReply() + { + // C: BLPOP key 1 + $message = "*-1\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals(null, $data); + } + + public function testParsingMultiBulkReplyWithMixedElements() + { + $message = "*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals(array(1, 2, 3, 4, 'foobar'), $data); + } + + public function testParsingMultiBulkReplyWithIncompletePush() + { + $this->assertCount(0, $this->parser->pushIncoming("*5\r\n:1\r\n:2\r")); + $this->assertCount(1, $models = $this->parser->pushIncoming("\n:3\r\n:4\r\n$6\r\nfoobar\r\n")); + + $data = reset($models)->getValueNative(); + $this->assertEquals(array(1, 2, 3, 4, 'foobar'), $data); + } + + public function testParsingMultiBulkReplyWithNullElement() + { + $message = "*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n"; + $this->assertCount(1, $models = $this->parser->pushIncoming($message)); + + $data = reset($models)->getValueNative(); + $this->assertEquals(array('foo', null, 'bar'), $data); + } + + /** + * @expectedException Clue\Redis\Protocol\Parser\ParserException + */ + public function testParseError() + { + $this->parser->pushIncoming("invalid string\r\n"); + } +} diff --git a/vendor/clue/redis-protocol/tests/Serializer/AbstractSerializerTest.php b/vendor/clue/redis-protocol/tests/Serializer/AbstractSerializerTest.php new file mode 100644 index 0000000..ba2200a --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Serializer/AbstractSerializerTest.php @@ -0,0 +1,141 @@ +<?php + +use Clue\Redis\Protocol\Serializer\SerializerInterface; +use Clue\Redis\Protocol\Model\Status; +use Clue\Redis\Protocol\Model\ErrorReplyException; +//use Exception; + +abstract class AbstractSerializerTest extends TestCase +{ + /** + * @return SerializerInterface + */ + abstract protected function createSerializer(); + + public function setUp() + { + $this->serializer = $this->createSerializer(); + } + + public function testIntegerReply() + { + $model = $this->serializer->createReplyModel(0); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\IntegerReply', $model); + $this->assertEquals(0, $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(0)); + } + + public function testFloatCastIntegerReply() + { + $model = $this->serializer->createReplyModel(-12.99); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\IntegerReply', $model); + $this->assertEquals(-12, $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(-12.99)); + + $model = $this->serializer->createReplyModel(14.99); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\IntegerReply', $model); + $this->assertEquals(14, $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(14.99)); + } + + public function testBooleanCastIntegerReply() + { + $model = $this->serializer->createReplyModel(true); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\IntegerReply', $model); + $this->assertEquals(1, $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(true)); + + $model = $this->serializer->createReplyModel(false); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\IntegerReply', $model); + $this->assertEquals(0, $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(false)); + } + + public function testStringReply() + { + $model = $this->serializer->createReplyModel('test'); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\BulkReply', $model); + $this->assertEquals('test', $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage('test')); + } + + public function testNullCastNullBulkReply() + { + $model = $this->serializer->createReplyModel(null); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\BulkReply', $model); + $this->assertEquals(null, $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(null)); + } + + public function testEmptyArrayMultiBulkReply() + { + $model = $this->serializer->createReplyModel(array()); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\MultiBulkReply', $model); + $this->assertEquals(array(), $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(array())); + } + + public function testArrayMultiBulkReply() + { + $model = $this->serializer->createReplyModel(array('test', 123)); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\MultiBulkReply', $model); + $this->assertEquals(array('test', 123), $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(array('test', 123))); + } + + public function testErrorReply() + { + $model = $this->serializer->createReplyModel(new Exception('ERR failure')); + $this->assertInstanceOf('Clue\Redis\Protocol\Model\ErrorReply', $model); + $this->assertEquals('ERR failure', $model->getValueNative()); + $this->assertEquals($model->getMessageSerialized($this->serializer), $this->serializer->getReplyMessage(new Exception('ERR failure'))); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidArgument() + { + $this->serializer->createReplyModel((object)array()); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidReplyData() + { + $this->serializer->getReplyMessage((object)array()); + } + + /** + * + * @param array $data + * @dataProvider provideRequestMessage + */ + public function testRequestMessage($command, $args) + { + // the model is already unit-tested, so just compare against its message + $model = $this->serializer->createRequestModel($command, $args); + + $message = $this->serializer->getRequestMessage($command, $args); + + $this->assertEquals($model->getMessageSerialized($this->serializer), $message); + } + + public function provideRequestMessage() + { + return array( + array('PING', array()), + array('GET', array('a')), + array('SET', array('a', 'b')), + array('SET', array('empty', '')) + ); + } + +// public function testBenchCreateRequest() +// { +// for ($i = 0; $i < 100000; ++$i) { +// $this->serializer->createReplyModel(array('a', 'b', 'c')); +// } +// } +} diff --git a/vendor/clue/redis-protocol/tests/Serializer/RecursiveSerializerTest.php b/vendor/clue/redis-protocol/tests/Serializer/RecursiveSerializerTest.php new file mode 100644 index 0000000..fe62ac5 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/Serializer/RecursiveSerializerTest.php @@ -0,0 +1,11 @@ +<?php + +use Clue\Redis\Protocol\Serializer\RecursiveSerializer; + +class RecursiveSerializerTest extends AbstractSerializerTest +{ + protected function createSerializer() + { + return new RecursiveSerializer(); + } +} diff --git a/vendor/clue/redis-protocol/tests/bootstrap.php b/vendor/clue/redis-protocol/tests/bootstrap.php new file mode 100644 index 0000000..0b2ea18 --- /dev/null +++ b/vendor/clue/redis-protocol/tests/bootstrap.php @@ -0,0 +1,7 @@ +<?php + +(include_once __DIR__ . '/../vendor/autoload.php') OR die(PHP_EOL . 'ERROR: composer autoloader not found, run "composer install" or see README for instructions' . PHP_EOL); + +class TestCase extends PHPUnit_Framework_TestCase +{ +} diff --git a/vendor/clue/redis-react/CHANGELOG.md b/vendor/clue/redis-react/CHANGELOG.md new file mode 100644 index 0000000..c5e8e66 --- /dev/null +++ b/vendor/clue/redis-react/CHANGELOG.md @@ -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 diff --git a/vendor/clue/redis-react/LICENSE b/vendor/clue/redis-react/LICENSE new file mode 100644 index 0000000..da15612 --- /dev/null +++ b/vendor/clue/redis-react/LICENSE @@ -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. diff --git a/vendor/clue/redis-react/README.md b/vendor/clue/redis-react/README.md new file mode 100644 index 0000000..5492572 --- /dev/null +++ b/vendor/clue/redis-react/README.md @@ -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. diff --git a/vendor/clue/redis-react/composer.json b/vendor/clue/redis-react/composer.json new file mode 100644 index 0000000..c1752cc --- /dev/null +++ b/vendor/clue/redis-react/composer.json @@ -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/" } + } +} diff --git a/vendor/clue/redis-react/src/Client.php b/vendor/clue/redis-react/src/Client.php new file mode 100644 index 0000000..ec54229 --- /dev/null +++ b/vendor/clue/redis-react/src/Client.php @@ -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(); +} diff --git a/vendor/clue/redis-react/src/Factory.php b/vendor/clue/redis-react/src/Factory.php new file mode 100644 index 0000000..4e94905 --- /dev/null +++ b/vendor/clue/redis-react/src/Factory.php @@ -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); + } +} diff --git a/vendor/clue/redis-react/src/LazyClient.php b/vendor/clue/redis-react/src/LazyClient.php new file mode 100644 index 0000000..d82b257 --- /dev/null +++ b/vendor/clue/redis-react/src/LazyClient.php @@ -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; + }); + } + } +} diff --git a/vendor/clue/redis-react/src/StreamingClient.php b/vendor/clue/redis-react/src/StreamingClient.php new file mode 100644 index 0000000..8afd84d --- /dev/null +++ b/vendor/clue/redis-react/src/StreamingClient.php @@ -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 + )); + } + } + } +} diff --git a/vendor/clue/soap-react/CHANGELOG.md b/vendor/clue/soap-react/CHANGELOG.md new file mode 100644 index 0000000..2b605d2 --- /dev/null +++ b/vendor/clue/soap-react/CHANGELOG.md @@ -0,0 +1,130 @@ +# Changelog + +## 2.0.0 (2020-10-28) + +* Feature / BC break: Update to reactphp/http v1.0.0. + (#45 by @SimonFrings) + +* Feature / BC break: Add type declarations and require PHP 7.1+ as a consequence + (#47 by @SimonFrings, #49 by @clue) + +* Use fully qualified class names in documentation. + (#46 by @SimonFrings) + +* Improve test suite and add `.gitattributes` to exclude dev files from export. + Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. + (#40 by @andreybolonin, #42 and #44 by @SimonFrings and #48 by @clue) + +## 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 diff --git a/vendor/clue/soap-react/LICENSE b/vendor/clue/soap-react/LICENSE new file mode 100644 index 0000000..9426ad3 --- /dev/null +++ b/vendor/clue/soap-react/LICENSE @@ -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. diff --git a/vendor/clue/soap-react/README.md b/vendor/clue/soap-react/README.md new file mode 100644 index 0000000..0984e13 --- /dev/null +++ b/vendor/clue/soap-react/README.md @@ -0,0 +1,453 @@ +# 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 order 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** + +* [Support us](#support-us) +* [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) + +## 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 query an example +web service via SOAP: + +```php +$loop = React\EventLoop\Factory::create(); +$browser = new React\Http\Browser($loop); +$wsdl = 'http://example.com/demo.wsdl'; + +$browser->get($wsdl)->then(function (Psr\Http\Message\ResponseInterface $response) use ($browser) { + $client = new Clue\React\Soap\Client($browser, (string)$response->getBody()); + $api = new Clue\React\Soap\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/reactphp/http#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 React\Http\Browser($loop); + +$wsdl = '<?xml …'; +$options = array(); + +$client = new Clue\React\Soap\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/reactphp/http#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 React\Http\Browser($loop, $connector); +$client = new Clue\React\Soap\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 React\Http\Browser($loop); + +$browser->get($url)->then( + function (Psr\Http\Message\ResponseInterface $response) use ($browser) { + // WSDL file is ready, create client + $client = new Clue\React\Soap\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 Clue\React\Soap\Client($browser, $wsdl); +} catch (SoapFault $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +} +``` + +> Note that if you have an old version of `ext-xdebug` < 2.7 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`](https://www.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 Clue\React\Soap\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 Clue\React\Soap\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 Clue\React\Soap\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 Clue\React\Soap\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/reactphp/http#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 Clue\React\Soap\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()`](https://www.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()`](https://www.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 Clue\React\Soap\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/http#timeouts) +like this: + +```php +$browser = new React\Http\Browser($loop); +$browser = $browser->withOptions(array( + 'timeout' => 10.0 +)); + +$client = new Clue\React\Soap\Client($browser, $wsdl); +$proxy = new Clue\React\Soap\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/http#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:^2.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 PHP 7.1+. + +## 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. diff --git a/vendor/clue/soap-react/composer.json b/vendor/clue/soap-react/composer.json new file mode 100644 index 0000000..c2a918e --- /dev/null +++ b/vendor/clue/soap-react/composer.json @@ -0,0 +1,29 @@ +{ + "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@clue.engineering" + } + ], + "autoload": { + "psr-4": { "Clue\\React\\Soap\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Soap\\": "tests/" } + }, + "require": { + "php": ">=7.1", + "react/http": "^1.0", + "react/promise": "^2.1 || ^1.2", + "ext-soap": "*" + }, + "require-dev": { + "clue/block-react": "^1.0", + "phpunit/phpunit": "^9.3 || ^7.5" + } +} diff --git a/vendor/clue/soap-react/src/Client.php b/vendor/clue/soap-react/src/Client.php new file mode 100644 index 0000000..dda7b98 --- /dev/null +++ b/vendor/clue/soap-react/src/Client.php @@ -0,0 +1,326 @@ +<?php + +namespace Clue\React\Soap; + +use Clue\React\Soap\Protocol\ClientDecoder; +use Clue\React\Soap\Protocol\ClientEncoder; +use Psr\Http\Message\ResponseInterface; +use React\Http\Browser; +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/reactphp/http#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 React\Http\Browser($loop); + * + * $wsdl = '<?xml …'; + * $options = array(); + * + * $client = new Clue\React\Soap\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/http#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 React\Http\Browser($loop, $connector); + * $client = new Clue\React\Soap\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 React\Http\Browser($loop); + * + * $browser->get($url)->then( + * function (Psr\Http\Message\ResponseInterface $response) use ($browser) { + * // WSDL file is ready, create client + * $client = new Clue\React\Soap\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 Clue\React\Soap\Client($browser, $wsdl); + * } catch (SoapFault $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * } + * ``` + * + * > Note that if you have an old version of `ext-xdebug` < 2.7 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`](https://www.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 Clue\React\Soap\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 Clue\React\Soap\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 Clue\React\Soap\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 Clue\React\Soap\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/http#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, ?string $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->withRejectErrorResponse(false); + $browser = $browser->withFollowRedirects(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 Clue\React\Soap\Proxy($client); + * $promise = $proxy->ping('hello', 42); + * ``` + * + * @param string $name + * @param mixed[] $args + * @return PromiseInterface Returns a Promise<mixed, Exception> + */ + public function soapCall(string $name, array $args): PromiseInterface + { + 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->request( + $request->getMethod(), + (string) $request->getUri(), + $request->getHeaders(), + (string) $request->getBody() + )->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()`](https://www.php.net/manual/en/soapclient.getfunctions.php). + * In non-WSDL mode, this method returns `null`. + * + * @return string[]|null + */ + public function getFunctions(): ?array + { + return $this->encoder->__getFunctions(); + } + + /** + * Returns an array of types defined in the WSDL. + * + * It returns the equivalent of PHP's + * [`SoapClient::__getTypes()`](https://www.php.net/manual/en/soapclient.gettypes.php). + * In non-WSDL mode, this method returns `null`. + * + * @return string[]|null + */ + public function getTypes(): ?array + { + 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): string + { + 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(string $location): self + { + $client = clone $this; + $client->encoder = clone $this->encoder; + $client->encoder->__setLocation($location); + + return $client; + } +} diff --git a/vendor/clue/soap-react/src/Protocol/ClientDecoder.php b/vendor/clue/soap-react/src/Protocol/ClientDecoder.php new file mode 100644 index 0000000..a4d7912 --- /dev/null +++ b/vendor/clue/soap-react/src/Protocol/ClientDecoder.php @@ -0,0 +1,51 @@ +<?php + +namespace Clue\React\Soap\Protocol; + +/** + * @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(string $function, string $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; + } +} diff --git a/vendor/clue/soap-react/src/Protocol/ClientEncoder.php b/vendor/clue/soap-react/src/Protocol/ClientEncoder.php new file mode 100644 index 0000000..8b065b9 --- /dev/null +++ b/vendor/clue/soap-react/src/Protocol/ClientEncoder.php @@ -0,0 +1,69 @@ +<?php + +namespace Clue\React\Soap\Protocol; + +use Psr\Http\Message\RequestInterface; +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 RequestInterface + * @throws \SoapFault if request is invalid according to WSDL + */ + public function encode(string $name, array $args): RequestInterface + { + $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 ''; + } +} diff --git a/vendor/clue/soap-react/src/Proxy.php b/vendor/clue/soap-react/src/Proxy.php new file mode 100644 index 0000000..9c1fd11 --- /dev/null +++ b/vendor/clue/soap-react/src/Proxy.php @@ -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 Clue\React\Soap\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(string $name, array $args): PromiseInterface + { + return $this->client->soapCall($name, $args); + } +} diff --git a/vendor/clue/socket-raw/CHANGELOG.md b/vendor/clue/socket-raw/CHANGELOG.md new file mode 100644 index 0000000..3a21b1b --- /dev/null +++ b/vendor/clue/socket-raw/CHANGELOG.md @@ -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 diff --git a/vendor/clue/socket-raw/LICENSE b/vendor/clue/socket-raw/LICENSE new file mode 100644 index 0000000..da15612 --- /dev/null +++ b/vendor/clue/socket-raw/LICENSE @@ -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. diff --git a/vendor/clue/socket-raw/README.md b/vendor/clue/socket-raw/README.md new file mode 100644 index 0000000..7ea79b3 --- /dev/null +++ b/vendor/clue/socket-raw/README.md @@ -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. diff --git a/vendor/clue/socket-raw/composer.json b/vendor/clue/socket-raw/composer.json new file mode 100644 index 0000000..2add27e --- /dev/null +++ b/vendor/clue/socket-raw/composer.json @@ -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" + } +} diff --git a/vendor/clue/socket-raw/src/Exception.php b/vendor/clue/socket-raw/src/Exception.php new file mode 100644 index 0000000..bdabf78 --- /dev/null +++ b/vendor/clue/socket-raw/src/Exception.php @@ -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; + } +} diff --git a/vendor/clue/socket-raw/src/Factory.php b/vendor/clue/socket-raw/src/Factory.php new file mode 100644 index 0000000..a5068f9 --- /dev/null +++ b/vendor/clue/socket-raw/src/Factory.php @@ -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; + } +} diff --git a/vendor/clue/socket-raw/src/Socket.php b/vendor/clue/socket-raw/src/Socket.php new file mode 100644 index 0000000..67407f2 --- /dev/null +++ b/vendor/clue/socket-raw/src/Socket.php @@ -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; + } +} diff --git a/vendor/clue/socks-react/CHANGELOG.md b/vendor/clue/socks-react/CHANGELOG.md new file mode 100644 index 0000000..cd72e1d --- /dev/null +++ b/vendor/clue/socks-react/CHANGELOG.md @@ -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) diff --git a/vendor/clue/socks-react/LICENSE b/vendor/clue/socks-react/LICENSE new file mode 100644 index 0000000..8efa9aa --- /dev/null +++ b/vendor/clue/socks-react/LICENSE @@ -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. diff --git a/vendor/clue/socks-react/README.md b/vendor/clue/socks-react/README.md new file mode 100644 index 0000000..c886a23 --- /dev/null +++ b/vendor/clue/socks-react/README.md @@ -0,0 +1,1116 @@ +# clue/reactphp-socks + +[![CI status](https://github.com/clue/reactphp-socks/actions/workflows/ci.yml/badge.svg)](https://github.com/clue/reactphp-socks/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/clue/socks-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/socks-react) + +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](https://reactphp.org/). + +The SOCKS proxy protocol family (SOCKS5, SOCKS4 and SOCKS4a) is commonly used to +tunnel HTTP(S) traffic through an intermediary ("proxy"), to conceal the origin +address (anonymity) or to circumvent address blocking (geoblocking). While many +(public) SOCKS proxy servers often limit this to HTTP(S) port `80` and `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 SOCKS proxy support to pretty much any +existing higher-level protocol implementation. +Besides the client side, it also provides a simple SOCKS server implementation +which allows you to build your own SOCKS proxy servers with custom business logic. + +* **Async execution of connections** - + Send any number of SOCKS 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) + * [Client](#client) + * [Plain TCP connections](#plain-tcp-connections) + * [Secure TLS connections](#secure-tls-connections) + * [HTTP requests](#http-requests) + * [Protocol version](#protocol-version) + * [DNS resolution](#dns-resolution) + * [Authentication](#authentication) + * [Proxy chaining](#proxy-chaining) + * [Connection timeout](#connection-timeout) + * [SOCKS over TLS](#socks-over-tls) + * [Unix domain sockets](#unix-domain-sockets) + * [Server](#server) + * [Server connector](#server-connector) + * [Authentication](#server-authentication) + * [Proxy chaining](#server-proxy-chaining) + * [SOCKS over TLS](#server-socks-over-tls) + * [Unix domain sockets](#server-unix-domain-sockets) +* [Servers](#servers) + * [Using a PHP SOCKS server](#using-a-php-socks-server) + * [Using SSH as a SOCKS server](#using-ssh-as-a-socks-server) + * [Using the Tor (anonymity network) to tunnel SOCKS connections](#using-the-tor-anonymity-network-to-tunnel-socks-connections) +* [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 + +Once [installed](#install), you can use the following code to send a secure +HTTPS request to google.com through a local SOCKS proxy server: + +```php +<?php + +require __DIR__ . '/vendor/autoload.php'; + +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$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; +}); +``` + +If you're not already running any other [SOCKS proxy server](#servers), +you can use the following code to create a SOCKS +proxy server listening for connections on `127.0.0.1:1080`: + +```php +<?php + +require __DIR__ . '/vendor/autoload.php'; + +// start a new SOCKS proxy server +$socks = new Clue\React\Socks\Server(); + +// listen on 127.0.0.1:1080 +$socket = new React\Socket\SocketServer('127.0.0.1:1080'); +$socks->listen($socket); +``` + +See also the [examples](examples). + +## Usage + +### Client + +The `Client` is responsible for communication with your SOCKS server instance. + +Its constructor simply accepts a SOCKS proxy URI with the SOCKS proxy server address: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); +``` + +You can omit the port if you're using the default SOCKS port 1080: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1'); +``` + +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' + ) +)); + +$proxy = new Clue\React\Socks\Client('my-socks-server.local:1080', $connector); +``` + +This is one of the two main classes 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 SOCKS proxy support to pretty much any +higher-level component: + +```diff +- $acme = new AcmeApi($connector); ++ $proxy = new Clue\React\Socks\Client('127.0.0.1:1080', $connector); ++ $acme = new AcmeApi($proxy); +``` + +#### Plain TCP connections + +SOCKS proxies are most frequently used to issue HTTP(S) 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\Socks\Client('127.0.0.1:1080'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + echo 'connected to www.google.com:80'; + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +You can either use the `Client` directly or you may want to wrap this connector +in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector): + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$connector->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + echo 'connected to www.google.com:80'; + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +See also the [first example](examples). + +The `tcp://` scheme can also be omitted. +Passing any other scheme will reject the promise. + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $connector->connect($uri); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying TCP/IP +connection to the SOCKS server and/or the SOCKS protocol negotiation and reject +the resulting promise. + +#### 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\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$connector->connect('tls://www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + // proceed with just the plain text data + // everything is encrypted/decrypted automatically + echo 'connected to SSL encrypted www.google.com'; + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +See also the [second example](examples). + +Pending connection attempts can be cancelled by canceling its pending promise +as usual. + +> Note how secure TLS connections are in fact entirely handled outside of + this SOCKS client implementation. + +You can optionally pass additional +[SSL context options](http://php.net/manual/en/context.ssl.php) +to the constructor like this: + +```php +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ), + 'dns' => false +)); +``` + +#### HTTP requests + +This library also allows you to send +[HTTP requests through a SOCKS proxy server](https://github.com/reactphp/http#socks-proxy). + +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\Socks\Client('127.0.0.1:1080'); + +$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. + +#### Protocol version + +This library supports the SOCKS5 and SOCKS4(a) protocol versions. +It focuses on the most commonly used core feature of connecting to a destination +host through the SOCKS proxy server. In this mode, a SOCKS proxy server acts as +a generic proxy allowing higher level application protocols to work through it. + +<table> + <tr> + <th></th> + <th>SOCKS5</th> + <th>SOCKS4(a)</th> + </tr> + <tr> + <th>Protocol specification</th> + <td><a href="https://tools.ietf.org/html/rfc1928">RFC 1928</a></td> + <td> + <a href="https://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol">SOCKS4.protocol</a> / + <a href="https://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4A.protocol">SOCKS4A.protocol</a> + </td> + </tr> + <tr> + <th>Tunnel outgoing TCP/IP connections</th> + <td>✓</td> + <td>✓</td> + </tr> + <tr> + <th><a href="#dns-resolution">Remote DNS resolution</a></th> + <td>✓</td> + <td>✗ / ✓</td> + </tr> + <tr> + <th>IPv6 addresses</th> + <td>✓</td> + <td>✗</td> + </tr> + <tr> + <th><a href="#authentication">Username/Password authentication</a></th> + <td>✓ (as per <a href="https://tools.ietf.org/html/rfc1929">RFC 1929</a>)</td> + <td>✗</td> + </tr> + <tr> + <th>Handshake # roundtrips</th> + <td>2 (3 with authentication)</td> + <td>1</td> + </tr> + <tr> + <th>Handshake traffic<br />+ remote DNS</th> + <td><em>variable</em> (+ auth + IPv6)<br />+ hostname - 3</td> + <td>17 bytes<br />+ hostname + 1</td> + </tr> + <tr> + <th>Incoming BIND requests</th> + <td><em>not implemented</em></td> + <td><em>not implemented</em></td> + </tr> + <tr> + <th>UDP datagrams</th> + <td><em>not implemented</em></td> + <td>✗</td> + </tr> + <tr> + <th>GSSAPI authentication</th> + <td><em>not implemented</em></td> + <td>✗</td> + </tr> +</table> + +By default, the `Client` communicates via SOCKS5 with the SOCKS server. +This is done because SOCKS5 is the latest version from the SOCKS protocol family +and generally has best support across other vendors. +You can also omit the default `socks://` URI scheme. Similarly, the `socks5://` +URI scheme acts as an alias for the default `socks://` URI scheme. + +```php +// all three forms are equivalent +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); +$proxy = new Clue\React\Socks\Client('socks://127.0.0.1:1080'); +$proxy = new Clue\React\Socks\Client('socks5://127.0.0.1:1080'); +``` + +If want to explicitly set the protocol version to SOCKS4(a), you can use the URI +scheme `socks4://` as part of the SOCKS URI: + +```php +$proxy = new Clue\React\Socks\Client('socks4://127.0.0.1:1080'); +``` + +#### DNS resolution + +By default, the `Client` does not perform any DNS resolution at all and simply +forwards any hostname you're trying to connect to to the SOCKS server. +The remote SOCKS server is thus responsible for looking up any hostnames via DNS +(this default mode is thus called *remote DNS resolution*). +As seen above, this mode is supported by the SOCKS5 and SOCKS4a protocols, but +not the original SOCKS4 protocol, as the protocol lacks a way to communicate hostnames. + +On the other hand, all SOCKS protocol versions support sending destination IP +addresses to the SOCKS 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 `Client` 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 (in particular when using the +[Tor network](#using-the-tor-anonymity-network-to-tunnel-socks-connections)). + +As noted above, the `Client` defaults to using remote DNS resolution. +However, wrapping the `Client` 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\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); +``` + +If you want to explicitly use *local DNS resolution* (such as when explicitly +using SOCKS4), you can use the following code: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +// 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' +)); +``` + +See also the [fourth example](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise +as usual. + +> Note how local DNS resolution is in fact entirely handled outside of this + SOCKS client implementation. + +#### Authentication + +This library supports username/password authentication for SOCKS5 servers as +defined in [RFC 1929](https://tools.ietf.org/html/rfc1929). + +On the client side, simply pass your username and password to use for +authentication (see below). +For each further connection the client will merely send a flag to the server +indicating authentication information is available. +Only if the server requests authentication during the initial handshake, +the actual authentication credentials will be transmitted to the server. + +Note that the password is transmitted in cleartext to the SOCKS proxy server, +so this methods should not be used on a network where you have to worry about eavesdropping. + +You can simply pass the authentication information as part of the SOCKS URI: + +```php +$proxy = new Clue\React\Socks\Client('alice:password@127.0.0.1:1080'); +``` + +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:1080'; + +$proxy = new Clue\React\Socks\Client($url); +``` + +> The authentication details will be transmitted in cleartext to the SOCKS proxy + server only if it requires username/password authentication. + If the authentication details are missing or not accepted by the remote SOCKS + proxy server, it is expected to reject each connection attempt with an + exception error code of `SOCKET_EACCES` (13). + +Authentication is only supported by protocol version 5 (SOCKS5), +so passing authentication to the `Client` enforces communication with protocol +version 5 and complains if you have explicitly set anything else: + +```php +// throws InvalidArgumentException +new Clue\React\Socks\Client('socks4://alice:password@127.0.0.1:1080'); +``` + +#### Proxy chaining + +The `Client` is responsible for creating connections to the SOCKS server which +then connects to the target host. + +``` +Client -> SocksServer -> TargetHost +``` + +Sometimes it may be required to establish outgoing connections via another SOCKS +server. +For example, this can be useful if you want to conceal your origin address. + +``` +Client -> MiddlemanSocksServer -> TargetSocksServer -> TargetHost +``` + +The `Client` uses any instance of the `ConnectorInterface` to establish +outgoing connections. +In order to connect through another SOCKS server, you can simply use another +SOCKS connector from another SOCKS client like this: + +```php +// https via the proxy chain "MiddlemanSocksServer -> TargetSocksServer -> TargetHost" +// please note how the client uses TargetSocksServer (not MiddlemanSocksServer!), +// which in turn then uses MiddlemanSocksServer. +// this creates a TCP/IP connection to MiddlemanSocksServer, which then connects +// to TargetSocksServer, which then connects to the TargetHost +$middle = new Clue\React\Socks\Client('127.0.0.1:1080'); +$target = new Clue\React\Socks\Client('example.com:1080', $middle); + +$connector = new React\Socket\Connector(array( + 'tcp' => $target, + 'dns' => false +)); + +$connector->connect('tls://www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + // … +}); +``` + +See also the [third example](examples). + +Pending connection attempts can be canceled by canceling its pending promise +as usual. + +Proxy chaining can happen on the server side and/or the client side: + +* If you ask your client to chain through multiple proxies, then each proxy + server does not really know anything about chaining at all. + This means that this is a client-only property. + +* If you ask your server to chain through another proxy, then your client does + not really know anything about chaining at all. + This means that this is a server-only property and not part of this class. + For example, you can find this in the below [`Server`](#server-proxy-chaining) + class or somewhat similar when you're using the + [Tor network](#using-the-tor-anonymity-network-to-tunnel-socks-connections). + +#### Connection timeout + +By default, the `Client` 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\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false, + 'timeout' => 3.0 +)); + +$connector->connect('tcp://google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + // connection succeeded within 3.0 seconds +}); +``` + +See also any of the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise +as usual. + +> Note how connection timeout is in fact entirely handled outside of this + SOCKS client implementation. + +#### SOCKS over TLS + +All [SOCKS protocol versions](#protocol-version) support forwarding TCP/IP +based connections and higher level protocols. +This implies that you can also use [secure TLS connections](#secure-tls-connections) +to transfer sensitive data across SOCKS proxy servers. +This means that no eavesdropper nor the proxy server will be able to decrypt +your data. + +However, the initial SOCKS communication between the client and the proxy is +usually via an unencrypted, plain TCP/IP connection. +This means that an eavesdropper may be able to see *where* you connect to and +may also be able to see your [SOCKS authentication](#authentication) details +in cleartext. + +As an alternative, you may establish a secure TLS connection to your SOCKS +proxy before starting the initial SOCKS communication. +This means that no eavesdroppper will be able to see the destination address +you want to connect to or your [SOCKS authentication](#authentication) details. + +You can use the `sockss://` URI scheme or use an explicit +[SOCKS protocol version](#protocol-version) like this: + +```php +$proxy = new Clue\React\Socks\Client('sockss://127.0.0.1:1080'); + +$proxy = new Clue\React\Socks\Client('socks4s://127.0.0.1:1080'); +``` + +See also [example 32](examples). + +Similarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$proxy = new Clue\React\Socks\Client('sockss://alice:password@127.0.0.1:1080'); +``` + +> Note that for most use cases, [secure TLS connections](#secure-tls-connections) + should be used instead. SOCKS over TLS is considered advanced usage and is + used very rarely in practice. + In particular, the SOCKS server has to accept secure TLS connections, see + also [Server SOCKS over TLS](#server-socks-over-tls) for more details. + Also, PHP does not support "double encryption" over a single connection. + This means that enabling [secure TLS connections](#secure-tls-connections) + over a communication channel that has been opened with SOCKS over TLS + may not be supported. + +> Note that the SOCKS protocol does not support the notion of TLS. The above + works reasonably well because TLS is only used for the connection between + client and proxy server and the SOCKS protocol data is otherwise identical. + This implies that this may also have only limited support for + [proxy chaining](#proxy-chaining) over multiple TLS paths. + +#### Unix domain sockets + +All [SOCKS protocol versions](#protocol-version) support forwarding TCP/IP +based connections and higher level protocols. +In some advanced cases, it may be useful to let your SOCKS 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 use the `socks+unix://` URI scheme or use an explicit +[SOCKS protocol version](#protocol-version) like this: + +```php +$proxy = new Clue\React\Socks\Client('socks+unix:///tmp/proxy.sock'); + +$proxy = new Clue\React\Socks\Client('socks4+unix:///tmp/proxy.sock'); +``` + +Similarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$proxy = new Clue\React\Socks\Client('socks+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 SOCKS 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 also not support [proxy chaining](#proxy-chaining) + over multiple UDS paths. + +### Server + +The `Server` is responsible for accepting incoming communication from SOCKS clients +and forwarding the requested connection to the target host. +It supports the SOCKS5 and SOCKS4(a) protocol versions by default. +You can start listening on an underlying TCP/IP socket server like this: + +```php +$socks = new Clue\React\Socks\Server(); + +// listen on 127.0.0.1:1080 +$socket = new React\Socket\SocketServer('127.0.0.1:1080'); +$socks->listen($socket); +``` + +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. + +Additionally, the `Server` constructor accepts optional parameters to explicitly +configure the [connector](#server-connector) to use and to require +[authentication](#server-authentication). For more details, read on... + +#### Server connector + +The `Server` uses an instance of ReactPHP's +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) +to establish outgoing connections for each incoming connection request. + +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' + ) +)); + +$socks = new Clue\React\Socks\Server(null, $connector); +``` + +If you want to forward the outgoing connection through another SOCKS proxy, you +may also pass a [`Client`](#client) instance as a connector, see also +[server proxy chaining](#server-proxy-chaining) for more details. + +Internally, the `Server` uses ReactPHP's normal +[`connect()`](https://github.com/reactphp/socket#connect) method, but +it also passes the original client IP as the `?source={remote}` parameter. +The `source` parameter contains the full remote URI, including the protocol +and any authentication details, for example `socks://alice:password@1.2.3.4:5678` +or `socks4://1.2.3.4:5678` for legacy SOCKS4(a). +You can use this parameter for logging purposes or to restrict connection +requests for certain clients by providing a custom implementation of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface). + +#### Server authentication + +By default, the `Server` does not require any authentication from the clients. +You can enable authentication support so that clients need to pass a valid +username and password before forwarding any connections. + +Setting authentication on the `Server` enforces each further connected client +to use protocol version 5 (SOCKS5). +If a client tries to use any other protocol version, does not send along +authentication details or if authentication details can not be verified, +the connection will be rejected. + +If you only want to accept static authentication details, you can simply pass an +additional assoc array with your authentication details to the `Server` like this: + +```php +$socks = new Clue\React\Socks\Server(null, null, array( + 'alice' => 'password', + 'bob' => 's3cret!1' +)); +``` + +See also [example #12](examples). + +If you want more control over authentication, you can pass an authenticator +function that should return a `bool` value like this synchronous example: + +```php +$socks = new Clue\React\Socks\Server(null, null, function ($username, $password, $remote) { + // $remote is a full URI à la socks://alice:password@192.168.1.1:1234 + // or sockss://alice:password@192.168.1.1:1234 for SOCKS over TLS + // or may be null when remote is unknown (SOCKS over Unix Domain Sockets) + // useful for logging or extracting parts, such as the remote IP + $ip = parse_url($remote, PHP_URL_HOST); + + return ($username === 'root' && $password === 'secret' && $ip === '127.0.0.1'); +}); +``` + +Because your authentication mechanism might take some time to actually check the +provided authentication credentials (like querying a remote database or webservice), +the server also supports a [Promise](https://github.com/reactphp/promise)-based +interface. While this might seem more complex at first, it actually provides a +very powerful way of handling a large number of connections concurrently without +ever blocking any connections. You can return a [Promise](https://github.com/reactphp/promise) +from the authenticator function that will fulfill with a `bool` value like this +async example: + +```php +$socks = new Clue\React\Socks\Server(null, null, function ($username, $password) use ($db) { + // pseudo-code: query database for given authentication details + return $db->query( + 'SELECT 1 FROM users WHERE name = ? AND password = ?', + array($username, $password) + )->then(function (QueryResult $result) { + // ensure we find exactly one match in the database + return count($result->resultRows) === 1; + }); +}); +``` + +#### Server proxy chaining + +The `Server` is responsible for creating connections to the target host. + +``` +Client -> SocksServer -> TargetHost +``` + +Sometimes it may be required to establish outgoing connections via another SOCKS +server. +For example, this can be useful if your target SOCKS server requires +authentication, but your client does not support sending authentication +information (e.g. like most webbrowser). + +``` +Client -> MiddlemanSocksServer -> TargetSocksServer -> TargetHost +``` + +The `Server` uses any instance of the `ConnectorInterface` to establish outgoing +connections. +In order to connect through another SOCKS server, you can simply use the +[`Client`](#client) SOCKS connector from above. +You can create a SOCKS `Client` instance like this: + +```php +// set next SOCKS server example.com:1080 as target +$proxy = new Clue\React\Socks\Client('alice:password@example.com:1080'); + +// start a new server which forwards all connections to the other SOCKS server +$socks = new Clue\React\Socks\Server(null, $proxy); + +// listen on 127.0.0.1:1080 +$socket = new React\Socket\SocketServer('127.0.0.1:1080'); +$socks->listen($socket); +``` + +See also [example #21](examples). + +Proxy chaining can happen on the server side and/or the client side: + +* If you ask your client to chain through multiple proxies, then each proxy + server does not really know anything about chaining at all. + This means that this is a client-only property and not part of this class. + For example, you can find this in the above [`Client`](#proxy-chaining) class. + +* If you ask your server to chain through another proxy, then your client does + not really know anything about chaining at all. + This means that this is a server-only property and can be implemented as above. + +#### Server SOCKS over TLS + +Both SOCKS5 and SOCKS4(a) protocol versions support forwarding TCP/IP based +connections and higher level protocols. +This implies that you can also use [secure TLS connections](#secure-tls-connections) +to transfer sensitive data across SOCKS proxy servers. +This means that no eavesdropper nor the proxy server will be able to decrypt +your data. + +However, the initial SOCKS communication between the client and the proxy is +usually via an unencrypted, plain TCP/IP connection. +This means that an eavesdropper may be able to see *where* the client connects +to and may also be able to see the [SOCKS authentication](#authentication) +details in cleartext. + +As an alternative, you may listen for SOCKS over TLS connections so +that the client has to establish a secure TLS connection to your SOCKS +proxy before starting the initial SOCKS communication. +This means that no eavesdroppper will be able to see the destination address +the client wants to connect to or their [SOCKS authentication](#authentication) +details. + +You can simply start your listening socket on the `tls://` URI scheme like this: + +```php +$socks = new Clue\React\Socks\Server(); + +// listen on tls://127.0.0.1:1080 with the given server certificate +$socket = new React\Socket\SocketServer('tls://127.0.0.1:1080', array( + 'tls' => array( + 'local_cert' => __DIR__ . '/localhost.pem', + ) +)); +$socks->listen($socket); +``` + +See also [example 31](examples). + +> Note that for most use cases, [secure TLS connections](#secure-tls-connections) + should be used instead. SOCKS over TLS is considered advanced usage and is + used very rarely in practice. + +> Note that the SOCKS protocol does not support the notion of TLS. The above + works reasonably well because TLS is only used for the connection between + client and proxy server and the SOCKS protocol data is otherwise identical. + This implies that this does also not support [proxy chaining](#server-proxy-chaining) + over multiple TLS paths. + +#### Server Unix domain sockets + +Both SOCKS5 and SOCKS4(a) protocol versions support forwarding TCP/IP based +connections and higher level protocols. +In some advanced cases, it may be useful to let your SOCKS 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](#server-authentication). + +You can simply start your listening socket on the `unix://` URI scheme like this: + +```php +$socks = new Clue\React\Socks\Server(); + +// listen on /tmp/proxy.sock +$socket = new React\Socket\SocketServer('unix:///tmp/proxy.sock'); +$socks->listen($socket); +``` + +> Note that Unix domain sockets (UDS) are considered advanced usage and that + the SOCKS 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 also not support [proxy chaining](#server-proxy-chaining) + over multiple UDS paths. + +## Servers + +### Using a PHP SOCKS server + +* If you're looking for an end-user SOCKS server daemon, you may want to use + [LeProxy](https://leproxy.org/) or [clue/psocksd](https://github.com/clue/psocksd). +* If you're looking for a SOCKS server implementation, consider using + the above [`Server`](#server) class. + +### Using SSH as a SOCKS server + +If you already have an SSH server set up, you can easily use it as a SOCKS +tunnel end point. On your client, simply start your SSH client and use +the `-D <port>` option to start a local SOCKS server (quoting the man page: +a `local "dynamic" application-level port forwarding`). + +You can start a local SOCKS server by creating a loopback connection to your +local system if you already run an SSH daemon: + +```bash +ssh -D 1080 localhost +``` + +Alternatively, you can start a local SOCKS server tunneling through a given +remote host that runs an SSH daemon: + +```bash +ssh -D 1080 example.com +``` + +Now you can simply use this SSH SOCKS server like this: + +```PHP +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +Note that the above will allow all users on the local system to connect over +your SOCKS server without authentication which may or may not be what you need. +As an alternative, recent OpenSSH client versions also support +[Unix domain sockets](#unix-domain-sockets) (UDS) paths so that you can rely +on Unix file system permissions instead: + +```bash +ssh -D/tmp/proxy.sock example.com +``` + +Now you can simply use this SSH SOCKS server like this: + +```PHP +$proxy = new Clue\React\Socks\Client('socks+unix:///tmp/proxy.sock'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +> As an alternative to requiring this manual setup, you may also want to look + into using [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy) + which automatically creates this SSH tunnel for you. It provides an implementation of the same + [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + so that supporting either proxy protocol should be fairly trivial. + +### Using the Tor (anonymity network) to tunnel SOCKS connections + +The [Tor anonymity network](https://www.torproject.org/) client software is designed +to encrypt your traffic and route it over a network of several nodes to conceal its origin. +It presents a SOCKS5 and SOCKS4(a) interface on TCP port 9050 by default +which allows you to tunnel any traffic through the anonymity network: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:9050'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +In most common scenarios you probably want to stick to default +[remote DNS resolution](#dns-resolution) and don't want your client to resolve the target hostnames, +because you would leak DNS information to anybody observing your local traffic. +Also, Tor provides hidden services through an `.onion` pseudo top-level domain +which have to be resolved by Tor. + +## 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/socks-react:^1.4 +``` + +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 a number of 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 component](https://github.com/reactphp/socket). +* 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 a SOCKS5 / SOCKS4(a) proxy, you may also want to look into + using an HTTP CONNECT proxy instead. + You may want to use [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-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. +* As an alternative to a SOCKS5 / SOCKS4(a) 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 SOCKS server daemon, you may want to use + [LeProxy](https://leproxy.org/) or [clue/psocksd](https://github.com/clue/psocksd). diff --git a/vendor/clue/socks-react/composer.json b/vendor/clue/socks-react/composer.json new file mode 100644 index 0000000..2a26a7a --- /dev/null +++ b/vendor/clue/socks-react/composer.json @@ -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/" } + } +} diff --git a/vendor/clue/socks-react/src/Client.php b/vendor/clue/socks-react/src/Client.php new file mode 100644 index 0000000..5645c25 --- /dev/null +++ b/vendor/clue/socks-react/src/Client.php @@ -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'); + } + }); + } +} diff --git a/vendor/clue/socks-react/src/Server.php b/vendor/clue/socks-react/src/Server.php new file mode 100644 index 0000000..ca45015 --- /dev/null +++ b/vendor/clue/socks-react/src/Server.php @@ -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); + }); + } +} diff --git a/vendor/clue/socks-react/src/StreamReader.php b/vendor/clue/socks-react/src/StreamReader.php new file mode 100644 index 0000000..f01d252 --- /dev/null +++ b/vendor/clue/socks-react/src/StreamReader.php @@ -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; + } +} diff --git a/vendor/clue/stdio-react/CHANGELOG.md b/vendor/clue/stdio-react/CHANGELOG.md new file mode 100644 index 0000000..965d89b --- /dev/null +++ b/vendor/clue/stdio-react/CHANGELOG.md @@ -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. diff --git a/vendor/clue/stdio-react/LICENSE b/vendor/clue/stdio-react/LICENSE new file mode 100644 index 0000000..da15612 --- /dev/null +++ b/vendor/clue/stdio-react/LICENSE @@ -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. diff --git a/vendor/clue/stdio-react/README.md b/vendor/clue/stdio-react/README.md new file mode 100644 index 0000000..624ded0 --- /dev/null +++ b/vendor/clue/stdio-react/README.md @@ -0,0 +1,708 @@ +# clue/reactphp-stdio + +[![CI status](https://github.com/clue/reactphp-stdio/workflows/CI/badge.svg)](https://github.com/clue/reactphp-stdio/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/clue/stdio-react?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/clue/stdio-react) + +Async, event-driven and UTF-8 aware console input & output (STDIN, STDOUT) for +truly interactive CLI applications, built on top of [ReactPHP](https://reactphp.org/). + +You can use this library to build truly interactive and responsive command +line (CLI) applications, that immediately react when the user types in +a line or hits a certain key. Inspired by `ext-readline`, but supports UTF-8 +and interleaved I/O (typing while output is being printed), history and +autocomplete support and takes care of proper TTY settings under the hood +without requiring any extensions or special installation. + +**Table of contents** + +* [Support us](#support-us) +* [Quickstart example](#quickstart-example) +* [Usage](#usage) + * [Stdio](#stdio) + * [Output](#output) + * [Input](#input) + * [Prompt](#prompt) + * [Echo](#echo) + * [Input buffer](#input-buffer) + * [Cursor](#cursor) + * [History](#history) + * [Autocomplete](#autocomplete) + * [Keys](#keys) + * [Bell](#bell) + * [~~Readline~~](#readline) +* [Pitfalls](#pitfalls) +* [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 + +Once [installed](#install), you can use the following code to present a prompt in a CLI program: + +```php +<?php + +require __DIR__ . '/vendor/autoload.php'; + +$stdio = new Clue\React\Stdio\Stdio(); +$stdio->setPrompt('Input > '); + +$stdio->on('data', function ($line) use ($stdio) { + $line = rtrim($line, "\r\n"); + $stdio->write('Your input: ' . $line . PHP_EOL); + + if ($line === 'quit') { + $stdio->end(); + } +}); +``` + +See also the [examples](examples). + +## Usage + +### Stdio + +The `Stdio` is the main interface to this library. +It is responsible for orchestrating the input and output streams +by registering and forwarding the corresponding events. + +```php +$stdio = new Clue\React\Stdio\Stdio(); +``` + +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. + +See below for waiting for user input and writing output. +The `Stdio` class is a well-behaving duplex stream +(implementing ReactPHP's `DuplexStreamInterface`) that emits each complete +line as a `data` event, including the trailing newline. + +#### Output + +The `Stdio` is a well-behaving writable stream +implementing ReactPHP's `WritableStreamInterface`. + +The `write($text)` method can be used to print the given text characters to console output. +This is useful if you need more control or want to output individual bytes or binary output: + +```php +$stdio->write('hello'); +$stdio->write(" world\n"); +``` + +Because the `Stdio` is a well-behaving writable stream, +you can also `pipe()` any readable stream into this stream. + +```php +$logger->pipe($stdio); +``` + +#### Input + +The `Stdio` is a well-behaving readable stream +implementing ReactPHP's `ReadableStreamInterface`. + +It will emit a `data` event for every line read from console input. +The event will contain the input buffer as-is, including the trailing newline. +You can register any number of event handlers like this: + +```php +$stdio->on('data', function ($line) { + if ($line === "start\n") { + doSomething(); + } +}); +``` + +Note that this class takes care of buffering incomplete lines and will only emit +complete lines. +This means that the line will usually end with the trailing newline character. +If the stream ends without a trailing newline character, it will not be present +in the `data` event. +As such, it's usually recommended to remove the trailing newline character +before processing command line input like this: + +```php +$stdio->on('data', function ($line) { + $line = rtrim($line, "\r\n"); + if ($line === "start") { + doSomething(); + } +}); +``` + +Similarly, if you copy and paste a larger chunk of text, it will properly emit +multiple complete lines with a separate `data` event for each line. + +Because the `Stdio` is a well-behaving readable stream that will emit incoming +data as-is, you can also use this to `pipe()` this stream into other writable +streams. + +```php +$stdio->pipe($logger); +``` + +You can control various aspects of the console input through this interface, +so read on.. + +#### Prompt + +The *prompt* will be written at the beginning of the *user input line*, right before the *user input buffer*. + +The `setPrompt($prompt)` method can be used to change the input prompt. +The prompt will be printed to the *user input line* as-is, so you will likely want to end this with a space: + +```php +$stdio->setPrompt('Input: '); +``` + +The default input prompt is empty, i.e. the *user input line* contains only the actual *user input buffer*. +You can restore this behavior by passing an empty prompt: + +```php +$stdio->setPrompt(''); +``` + +The `getPrompt()` method can be used to get the current input prompt. +It will return an empty string unless you've set anything else: + +```php +assert($stdio->getPrompt() === ''); +``` + +#### Echo + +The *echo mode* controls how the actual *user input buffer* will be presented in the *user input line*. + +The `setEcho($echo)` method can be used to control the echo mode. +The default is to print the *user input buffer* as-is. + +You can disable printing the *user input buffer*, e.g. for password prompts. +The user will still be able to type, but will not receive any indication of the current *user input buffer*. +Please note that this often leads to a bad user experience as users will not even see their cursor position. +Simply pass a boolean `false` like this: + +```php +$stdio->setEcho(false); +``` + +Alternatively, you can also *hide* the *user input buffer* by using a replacement character. +One replacement character will be printed for each character in the *user input buffer*. +This is useful for password prompts to give users an indicatation that their key presses are registered. +This often provides a better user experience and allows users to still control their cursor position. +Simply pass a string replacement character likes this: + +```php +$stdio->setEcho('*'); +``` + +To restore the original behavior where every character appears as-is, simply pass a boolean `true`: + +```php +$stdio->setEcho(true); +``` + +#### Input buffer + +Everything the user types will be buffered in the current *user input buffer*. +Once the user hits enter, the *user input buffer* will be processed and cleared. + +The `addInput($input)` method can be used to add text to the *user input +buffer* at the current cursor position. +The given text will be inserted just like the user would type in a text and as +such adjusts the current cursor position accordingly. +The user will be able to delete and/or rewrite the buffer at any time. +Changing the *user input buffer* can be useful for presenting a preset input to +the user (like the last password attempt). +Simply pass an input string like this: + +```php +$stdio->addInput('hello'); +``` + +The `setInput($buffer)` method can be used to control the *user input buffer*. +The given text will be used to replace the entire current *user input buffer* +and as such adjusts the current cursor position to the end of the new buffer. +The user will be able to delete and/or rewrite the buffer at any time. +Changing the *user input buffer* can be useful for presenting a preset input to +the user (like the last password attempt). +Simply pass an input string like this: + +```php +$stdio->setInput('lastpass'); +``` + +The `getInput()` method can be used to access the current *user input buffer*. +This can be useful if you want to append some input behind the current *user input buffer*. +You can simply access the buffer like this: + +```php +$buffer = $stdio->getInput(); +``` + +#### Cursor + +By default, users can control their (horizontal) cursor position by using their arrow keys on the keyboard. +Also, every character pressed on the keyboard advances the cursor position. + +The `setMove($toggle)` method can be used to control whether users are allowed to use their arrow keys. +To disable the left and right arrow keys, simply pass a boolean `false` like this: + +```php +$stdio->setMove(false); +``` + +To restore the default behavior where the user can use the left and right arrow keys, +simply pass a boolean `true` like this: + +```php +$stdio->setMove(true); +``` + +The `getCursorPosition()` method can be used to access the current cursor position, +measured in number of characters. +This can be useful if you want to get a substring of the current *user input buffer*. +Simply invoke it like this: + +```php +$position = $stdio->getCursorPosition(); +``` + +The `getCursorCell()` method can be used to get the current cursor position, +measured in number of monospace cells. +Most *normal* characters (plain ASCII and most multi-byte UTF-8 sequences) take a single monospace cell. +However, there are a number of characters that have no visual representation +(and do not take a cell at all) or characters that do not fit within a single +cell (like some Asian glyphs). +This method is mostly useful for calculating the visual cursor position on screen, +but you may also invoke it like this: + +```php +$cell = $stdio->getCursorCell(); +``` + +The `moveCursorTo($position)` method can be used to set the current cursor position to the given absolute character position. +For example, to move the cursor to the beginning of the *user input buffer*, simply call: + +```php +$stdio->moveCursorTo(0); +``` + +The `moveCursorBy($offset)` method can be used to change the cursor position +by the given number of characters relative to the current position. +A positive number will move the cursor to the right - a negative number will move the cursor to the left. +For example, to move the cursor one character to the left, simply call: + +```php +$stdio->moveCursorBy(-1); +``` + +#### History + +By default, users can access the history of previous commands by using their +UP and DOWN cursor keys on the keyboard. +The history will start with an empty state, thus this feature is effectively +disabled, as the UP and DOWN cursor keys have no function then. + +Note that the history is not maintained automatically. +Any input the user submits by hitting enter will *not* be added to the history +automatically. +This may seem inconvenient at first, but it actually gives you more control over +what (and when) lines should be added to the history. +If you want to automatically add everything from the user input to the history, +you may want to use something like this: + +```php +$stdio->on('data', function ($line) use ($stdio) { + $line = rtrim($line); + $all = $stdio->listHistory(); + + // skip empty line and duplicate of previous line + if ($line !== '' && $line !== end($all)) { + $stdio->addHistory($line); + } +}); +``` + +The `listHistory(): string[]` method can be used to +return an array with all lines in the history. +This will be an empty array until you add new entries via `addHistory()`. + +```php +$list = $stdio->listHistory(); + +assert(count($list) === 0); +``` + +The `addHistory(string $line): void` method can be used to +add a new line to the (bottom position of the) history list. +A following `listHistory()` call will return this line as the last element. + +```php +$stdio->addHistory('a'); +$stdio->addHistory('b'); + +$list = $stdio->listHistory(); +assert($list === array('a', 'b')); +``` + +The `clearHistory(): void` method can be used to +clear the complete history list. +A following `listHistory()` call will return an empty array until you add new +entries via `addHistory()` again. +Note that the history feature will effectively be disabled if the history is +empty, as the UP and DOWN cursor keys have no function then. + +```php +$stdio->clearHistory(); + +$list = $stdio->listHistory(); +assert(count($list) === 0); +``` + +The `limitHistory(?int $limit): void` method can be used to +set a limit of history lines to keep in memory. +By default, only the last 500 lines will be kept in memory and everything else +will be discarded. +You can use an integer value to limit this to the given number of entries or +use `null` for an unlimited number (not recommended, because everything is +kept in RAM). +If you set the limit to `0` (int zero), the history will effectively be +disabled, as no lines can be added to or returned from the history list. +If you're building a CLI application, you may also want to use something like +this to obey the `HISTSIZE` environment variable: + +```php +$limit = getenv('HISTSIZE'); +if ($limit === '' || $limit < 0) { + // empty string or negative value means unlimited + $stdio->limitHistory(null); +} elseif ($limit !== false) { + // apply any other value if given + $stdio->limitHistory($limit); +} +``` + +There is no such thing as a `readHistory()` or `writeHistory()` method +because filesystem operations are inherently blocking and thus beyond the scope +of this library. +Using your favorite filesystem API and an appropriate number of `addHistory()` +or a single `listHistory()` call respectively should be fairly straight +forward and is left up as an exercise for the reader of this documentation +(i.e. *you*). + +#### Autocomplete + +By default, users can use autocompletion by using their TAB keys on the keyboard. +The autocomplete function is not registered by default, thus this feature is +effectively disabled, as the TAB key has no function then. + +The `setAutocomplete(?callable $autocomplete): void` method can be used to +register a new autocomplete handler. +In its most simple form, you won't need to assign any arguments and can simply +return an array of possible word matches from a callable like this: + +```php +$stdio->setAutocomplete(function () { + return array( + 'exit', + 'echo', + 'help', + ); +}); +``` + +If the user types `he [TAB]`, the first two matches will be skipped as they do +not match the current word prefix and the last one will be picked automatically, +so that the resulting input buffer is `hello `. + +If the user types `e [TAB]`, then this will match multiple entries and the user +will be presented with a list of up to 8 available word completions to choose +from like this: + +```php +> e [TAB] +exit echo +> e +``` + +Unless otherwise specified, the matches will be performed against the current +word boundaries in the input buffer. +This means that if the user types `hello [SPACE] ex [TAB]`, then the resulting +input buffer is `hello exit `, which may or may not be what you need depending +on your particular use case. + +In order to give you more control over this behavior, the autocomplete function +actually receives three arguments (similar to `ext-readline`'s +[`readline_completion_function()`](https://www.php.net/manual/en/function.readline-completion-function.php)): +The first argument will be the current incomplete word according to current +cursor position and word boundaries, while the second and third argument will be +the start and end offset of this word within the complete input buffer measured +in (Unicode) characters. +The above examples will be invoked as `$fn('he', 0, 2)`, `$fn('e', 0, 1)` and +`$fn('ex', 6, 8)` respectively. +You may want to use this as an `$offset` argument to check if the current word +is an argument or a root command and the `$word` argument to autocomplete +partial filename matches like this: + +```php +$stdio->setAutocomplete(function ($word, $offset) { + if ($offset <= 1) { + // autocomplete root commands at offset=0/1 only + return array('cat', 'rm', 'stat'); + } else { + // autocomplete all command arguments as glob pattern + return glob($word . '*', GLOB_MARK); + } +}); +``` + +> Note that the user may also use quotes and/or leading whitespace around the +root command, for example `"hell [TAB]`, in which case the offset will be +advanced such as this will be invoked as `$fn('hell', 1, 4)`. +Unless you use a more sophisticated argument parser, a decent approximation may +be using `$offset <= 1` to check this is a root command. + +If you need even more control over autocompletion, you may also want to access +and/or manipulate the [input buffer](#input-buffer) and [cursor](#cursor) +directly like this: + +```php +$stdio->setAutocomplete(function () use ($stdio) { + if ($stdio->getInput() === 'run') { + $stdio->setInput('run --test --value=42'); + $stdio->moveCursorBy(-2); + } + + // return empty array so normal autocompletion doesn't kick in + return array(); +}); +``` + +You can use a `null` value to remove the autocomplete function again and thus +disable the autocomplete function: + +```php +$stdio->setAutocomplete(null); +``` + +#### Keys + +The `Readline` class is responsible for reading user input from `STDIN` and +registering appropriate key events. +By default, `Readline` uses a hard-coded key mapping that resembles the one +usually found in common terminals. +This means that normal Unicode character keys ("a" and "b", but also "?", "ä", +"µ" etc.) will be processed as user input, while special control keys can be +used for [cursor movement](#cursor), [history](#history) and +[autocomplete](#autocomplete) functions. +Unknown special keys will be ignored and will not processed as part of the user +input by default. + +Additionally, you can bind custom functions to any key code you want. +If a custom function is bound to a certain key code, the default behavior will +no longer trigger. +This allows you to register entirely new functions to keys or to overwrite any +of the existing behavior. + +For example, you can use the following code to print some help text when the +user hits a certain key: + +```php +$stdio->on('?', function () use ($stdio) { + $stdio->write('Here\'s some help: …' . PHP_EOL); +}); +``` + +Similarly, this can be used to manipulate the user input and replace some of the +input when the user hits a certain key: + +```php +$stdio->on('ä', function () use ($stdio) { + $stdio->addInput('a'); +}); +``` + +The `Readline` uses raw binary key codes as emitted by the terminal. +This means that you can use the normal UTF-8 character representation for normal +Unicode characters. +Special keys use binary control code sequences (refer to ANSI / VT100 control +codes for more details). +For example, the following code can be used to register a custom function to the +UP arrow cursor key: + +```php +$stdio->on("\033[A", function () use ($stdio) { + $stdio->setInput(strtoupper($stdio->getInput())); +}); +``` + +#### Bell + +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. + +Whether or not the BELL is audible/visible depends on the termin and its +settings, i.e. some terminals may "beep" or flash the screen or emit a short +vibration. + +The `setBell(bool $bell): void` method can be used to +enable or disable emitting the BELL signal when using a disabled function: + +```php +$stdio->setBell(false); +``` + +### ~~Readline~~ + +> Deprecated since v2.3.0, see [`Stdio`](#stdio) instead. + +The deprecated `Readline` class is responsible for reacting to user input and +presenting a prompt to the user. It does so by reading individual bytes from the +input stream and writing the current *user input line* to the output stream. + +The deprecated `Readline` class is only used internally and should no longer be +referenced from consuming projects. + +You can access the current instance through the [`Stdio`](#stdio): + +```php +// deprecated +$readline = $stdio->getReadline(); +``` + +All methods that are available on the `Readline` instance are now available on +the `Stdio` class. For BC reasons, they remain available on the `Readline` class +until the next major release, see also above for more details. + +```php +// deprecated +$readline->setPrompt('> '); + +// new +$stdio->setPrompt('> '); +``` + +Internally, the `Readline` is also a well-behaving readable stream +(implementing ReactPHP's `ReadableStreamInterface`) that emits each complete +line as a `data` event, including the trailing newline. +This is considered advanced usage. + +## Pitfalls + +The [`Stdio`](#stdio) has to redraw the current user +input line whenever output is written to the `STDOUT`. +Because of this, it is important to make sure any output is always +written like this instead of using `echo` statements: + +```php +// echo 'hello world!' . PHP_EOL; +$stdio->write('hello world!' . PHP_EOL); +``` + +Depending on your program, it may or may not be reasonable to +replace all such occurences. +As an alternative, you may utilize output buffering that will +automatically forward all write events to the [`Stdio`](#stdio) +instance like this: + +```php +ob_start(function ($chunk) use ($stdio) { + // forward write event to Stdio instead + $stdio->write($chunk); + + // discard data from normal output handling + return ''; +}, 1); +``` + +## 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/stdio-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. + +Internally, it will use the `ext-mbstring` to count and measure string sizes. +If this extension is missing, then this library will use a slighty slower Regex +work-around that should otherwise work equally well. +Installing `ext-mbstring` is highly recommended. + +Internally, it will use the `ext-readline` to enable raw terminal input mode. +If this extension is missing, then this library will manually set the required +TTY settings on start and will try to restore previous settings on exit. +Input line editing is handled entirely within this library and does not rely on +`ext-readline`. +Installing `ext-readline` is entirely optional. + +Note that *Microsoft Windows is not supported*. +Due to platform constraints, PHP does not provide support for reading from +standard console input without blocking on Windows. +Unfortunately, until the underlying PHP feature request is implemented (which +is unlikely to happen any time soon), there's little we can do in this library. +However, this package does work on Windows Subsystem for Linux (or WSL) without +issues. We suggest [installing WSL](https://msdn.microsoft.com/en-us/commandline/wsl/install_guide) +when you want to run this package on Windows. +See also [#18](https://github.com/clue/reactphp-stdio/issues/18) for more details. + +## 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. + +## More + +* 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. + +* If you build an interactive CLI tool that reads a command line from STDIN, you + may want to use [clue/arguments](https://github.com/clue/php-arguments) in + order to split this string up into its individual arguments and then use + [clue/commander](https://github.com/clue/php-commander) to route to registered + commands and their required arguments. diff --git a/vendor/clue/stdio-react/composer.json b/vendor/clue/stdio-react/composer.json new file mode 100644 index 0000000..0e86dcb --- /dev/null +++ b/vendor/clue/stdio-react/composer.json @@ -0,0 +1,37 @@ +{ + "name": "clue/stdio-react", + "description": "Async, event-driven console input & output (STDIN, STDOUT) for truly interactive CLI applications, built on top of ReactPHP", + "keywords": ["stdio", "stdin", "stdout", "interactive", "CLI", "readline", "autocomplete", "autocompletion", "history", "ReactPHP", "async"], + "homepage": "https://github.com/clue/reactphp-stdio", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "require": { + "php": ">=5.3", + "clue/term-react": "^1.0 || ^0.1.1", + "clue/utf8-react": "^1.0 || ^0.1", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "clue/arguments": "^2.0", + "clue/commander": "^1.2", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35" + }, + "suggest": { + "ext-mbstring": "Using ext-mbstring should provide slightly better performance for handling I/O" + }, + "config": { + "sort-packages": true + }, + "autoload": { + "psr-4": { "Clue\\React\\Stdio\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Stdio\\": "tests/" } + } +} diff --git a/vendor/clue/stdio-react/src/Readline.php b/vendor/clue/stdio-react/src/Readline.php new file mode 100644 index 0000000..b75650e --- /dev/null +++ b/vendor/clue/stdio-react/src/Readline.php @@ -0,0 +1,1017 @@ +<?php + +namespace Clue\React\Stdio; + +use Clue\React\Term\ControlCodeParser; +use Clue\React\Utf8\Sequencer as Utf8Sequencer; +use Evenement\EventEmitter; +use Evenement\EventEmitterInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * @deprecated 2.3.0 Use `Stdio` instead + * @see Stdio + */ +class Readline extends EventEmitter implements ReadableStreamInterface +{ + private $prompt = ''; + private $linebuffer = ''; + private $linepos = 0; + private $echo = true; + private $move = true; + private $bell = true; + private $encoding = 'utf-8'; + + private $input; + private $output; + private $sequencer; + private $closed = false; + + private $historyLines = array(); + private $historyPosition = null; + private $historyUnsaved = null; + private $historyLimit = 500; + + private $autocomplete = null; + private $autocompleteSuggestions = 8; + + public function __construct(ReadableStreamInterface $input, WritableStreamInterface $output, EventEmitterInterface $base = null) + { + $this->input = $input; + $this->output = $output; + + if (!$this->input->isReadable()) { + $this->close(); + return; + } + // push input through control code parser + $parser = new ControlCodeParser($input); + + $that = $this; + $codes = array( + "\n" => 'onKeyEnter', // ^J + "\x7f" => 'onKeyBackspace', // ^? + "\t" => 'onKeyTab', // ^I + "\x04" => 'handleEnd', // ^D + + "\033[A" => 'onKeyUp', + "\033[B" => 'onKeyDown', + "\033[C" => 'onKeyRight', + "\033[D" => 'onKeyLeft', + + "\033[1~" => 'onKeyHome', +// "\033[2~" => 'onKeyInsert', + "\033[3~" => 'onKeyDelete', + "\033[4~" => 'onKeyEnd', + +// "\033[20~" => 'onKeyF10', + ); + $decode = function ($code) use ($codes, $that, $base) { + // The user confirms input with enter key which should usually + // generate a NL (`\n`) character. Common terminals also seem to + // accept a CR (`\r`) character in place and handle this just like a + // NL. Similarly `ext-readline` uses different `icrnl` and `igncr` + // TTY settings on some platforms, so we also accept CR as an alias + // for NL here. This implies key binding for NL will also trigger. + if ($code === "\r") { + $code = "\n"; + } + + // forward compatibility: check if any key binding exists on base Stdio instance + if ($base !== null && $base->listeners($code)) { + $base->emit($code, array($code)); + return; + } + + // deprecated: check if any key binding exists on this Readline instance + if ($that->listeners($code)) { + $that->emit($code, array($code)); + return; + } + + if (isset($codes[$code])) { + $method = $codes[$code]; + $that->$method($code); + return; + } + }; + + $parser->on('csi', $decode); + $parser->on('c0', $decode); + + // push resulting data through utf8 sequencer + $utf8 = new Utf8Sequencer($parser); + $utf8->on('data', function ($data) use ($that, $base) { + $that->onFallback($data, $base); + }); + + // process all stream events (forwarded from input stream) + $utf8->on('end', array($this, 'handleEnd')); + $utf8->on('error', array($this, 'handleError')); + $utf8->on('close', array($this, 'close')); + } + + /** + * prompt to prepend to input line + * + * Will redraw the current input prompt with the current input buffer. + * + * @param string $prompt + * @return self + * @uses self::redraw() + * @deprecated use Stdio::setPrompt() instead + */ + public function setPrompt($prompt) + { + if ($prompt === $this->prompt) { + return $this; + } + + $this->prompt = $prompt; + + return $this->redraw(); + } + + /** + * returns the prompt to prepend to input line + * + * @return string + * @see self::setPrompt() + * @deprecated use Stdio::getPrompt() instead + */ + public function getPrompt() + { + return $this->prompt; + } + + /** + * sets whether/how to echo text input + * + * The default setting is `true`, which means that every character will be + * echo'ed as-is, i.e. you can see what you're typing. + * For example: Typing "test" shows "test". + * + * You can turn this off by supplying `false`, which means that *nothing* + * will be echo'ed while you're typing. This could be a good idea for + * password prompts. Note that this could be confusing for users, so using + * a character replacement as following is often preferred. + * For example: Typing "test" shows "" (nothing). + * + * Alternative, you can supply a single character replacement character + * that will be echo'ed for each character in the text input. This could + * be a good idea for password prompts, where an asterisk character ("*") + * is often used to indicate typing activity and password length. + * For example: Typing "test" shows "****" (with asterisk replacement) + * + * Changing this setting will redraw the current prompt and echo the current + * input buffer according to the new setting. + * + * @param boolean|string $echo echo can be turned on (boolean true) or off (boolean true), or you can supply a single character replacement string + * @return self + * @uses self::redraw() + * @deprecated use Stdio::setEcho() instead + */ + public function setEcho($echo) + { + if ($echo === $this->echo) { + return $this; + } + + $this->echo = $echo; + + // only redraw if there is any input + if ($this->linebuffer !== '') { + $this->redraw(); + } + + return $this; + } + + /** + * whether or not to support moving cursor left and right + * + * switching cursor support moves the cursor to the end of the current + * input buffer (if any). + * + * @param boolean $move + * @return self + * @uses self::redraw() + * @deprecated use Stdio::setMove() instead + */ + public function setMove($move) + { + $this->move = !!$move; + + return $this->moveCursorTo($this->strlen($this->linebuffer)); + } + + /** + * Gets current cursor position measured in number of text characters. + * + * Note that the number of text characters doesn't necessarily reflect the + * number of monospace cells occupied by the text characters. If you want + * to know the latter, use `self::getCursorCell()` instead. + * + * @return int + * @see self::getCursorCell() to get the position measured in monospace cells + * @see self::moveCursorTo() to move the cursor to a given character position + * @see self::moveCursorBy() to move the cursor by given number of characters + * @see self::setMove() to toggle whether the user can move the cursor position + * @deprecated use Stdio::getCursorPosition() instead + */ + public function getCursorPosition() + { + return $this->linepos; + } + + /** + * Gets current cursor position measured in monospace cells. + * + * Note that the cell position doesn't necessarily reflect the number of + * text characters. If you want to know the latter, use + * `self::getCursorPosition()` instead. + * + * Most "normal" characters occupy a single monospace cell, i.e. the ASCII + * sequence for "A" requires a single cell, as do most UTF-8 sequences + * like "Ä". + * + * However, there are a number of code points that do not require a cell + * (i.e. invisible surrogates) or require two cells (e.g. some asian glyphs). + * + * Also note that this takes the echo mode into account, i.e. the cursor is + * always at position zero if echo is off. If using a custom echo character + * (like asterisk), it will take its width into account instead of the actual + * input characters. + * + * @return int + * @see self::getCursorPosition() to get current cursor position measured in characters + * @see self::moveCursorTo() to move the cursor to a given character position + * @see self::moveCursorBy() to move the cursor by given number of characters + * @see self::setMove() to toggle whether the user can move the cursor position + * @see self::setEcho() + * @deprecated use Stdio::getCursorCell() instead + */ + public function getCursorCell() + { + if ($this->echo === false) { + return 0; + } + if ($this->echo !== true) { + return $this->strwidth($this->echo) * $this->linepos; + } + return $this->strwidth($this->substr($this->linebuffer, 0, $this->linepos)); + } + + /** + * Moves cursor to right by $n chars (or left if $n is negative). + * + * Zero value or values out of range (exceeding current input buffer) are + * simply ignored. + * + * Will redraw() the readline only if the visible cell position changes, + * see `self::getCursorCell()` for more details. + * + * @param int $n + * @return self + * @uses self::moveCursorTo() + * @uses self::redraw() + * @deprecated use Stdio::moveCursorBy() instead + */ + public function moveCursorBy($n) + { + return $this->moveCursorTo($this->linepos + $n); + } + + /** + * Moves cursor to given position in current line buffer. + * + * Values out of range (exceeding current input buffer) are simply ignored. + * + * Will redraw() the readline only if the visible cell position changes, + * see `self::getCursorCell()` for more details. + * + * @param int $n + * @return self + * @uses self::redraw() + * @deprecated use Stdio::moveCursorTo() instead + */ + public function moveCursorTo($n) + { + if ($n < 0 || $n === $this->linepos || $n > $this->strlen($this->linebuffer)) { + return $this; + } + + $old = $this->getCursorCell(); + $this->linepos = $n; + + // only redraw if visible cell position change (implies cursor is actually visible) + if ($this->getCursorCell() !== $old) { + $this->redraw(); + } + + return $this; + } + + /** + * Appends the given input to the current text input buffer at the current position + * + * This moves the cursor accordingly to the number of characters added. + * + * @param string $input + * @return self + * @uses self::redraw() + * @deprecated use Stdio::addInput() instead + */ + public function addInput($input) + { + if ($input === '') { + return $this; + } + + // read everything up until before current position + $pre = $this->substr($this->linebuffer, 0, $this->linepos); + $post = $this->substr($this->linebuffer, $this->linepos); + + $this->linebuffer = $pre . $input . $post; + $this->linepos += $this->strlen($input); + + // only redraw if input should be echo'ed (i.e. is not hidden anyway) + if ($this->echo !== false) { + $this->redraw(); + } + + return $this; + } + + /** + * set current text input buffer + * + * this moves the cursor to the end of the current + * input buffer (if any). + * + * @param string $input + * @return self + * @uses self::redraw() + * @deprecated use Stdio::setInput() instead + */ + public function setInput($input) + { + if ($this->linebuffer === $input) { + return $this; + } + + // remember old input length if echo replacement is used + $oldlen = (is_string($this->echo)) ? $this->strlen($this->linebuffer) : null; + + $this->linebuffer = $input; + $this->linepos = $this->strlen($this->linebuffer); + + // only redraw if input should be echo'ed (i.e. is not hidden anyway) + // and echo replacement is used, make sure the input length changes + if ($this->echo !== false && $this->linepos !== $oldlen) { + $this->redraw(); + } + + return $this; + } + + /** + * get current text input buffer + * + * @return string + * @deprecated use Stdio::getInput() instead + */ + public function getInput() + { + return $this->linebuffer; + } + + /** + * Adds a new line to the (bottom position of the) history list + * + * @param string $line + * @return self + * @uses self::limitHistory() to make sure list does not exceed limits + * @deprecated use Stdio::addHistory() instead + */ + public function addHistory($line) + { + $this->historyLines []= $line; + + return $this->limitHistory($this->historyLimit); + } + + /** + * Clears the complete history list + * + * @return self + * @deprecated use Stdio::clearHistory() instead + */ + public function clearHistory() + { + $this->historyLines = array(); + $this->historyPosition = null; + + if ($this->historyUnsaved !== null) { + $this->setInput($this->historyUnsaved); + $this->historyUnsaved = null; + } + + return $this; + } + + /** + * Returns an array with all lines in the history + * + * @return string[] + * @deprecated use Stdio::listHistory() instead + */ + public function listHistory() + { + return $this->historyLines; + } + + /** + * Limits the history to a maximum of N entries and truncates the current history list accordingly + * + * @param int|null $limit + * @return self + * @deprecated use Stdio::limitHistory() instead + */ + public function limitHistory($limit) + { + $this->historyLimit = $limit === null ? null : $limit; + + // limit send and currently exceeded + if ($this->historyLimit !== null && isset($this->historyLines[$this->historyLimit])) { + // adjust position in history according to new position after applying limit + if ($this->historyPosition !== null) { + $this->historyPosition -= count($this->historyLines) - $this->historyLimit; + + // current position will drop off from list => restore original + if ($this->historyPosition < 0) { + $this->setInput($this->historyUnsaved); + $this->historyPosition = null; + $this->historyUnsaved = null; + } + } + + $this->historyLines = array_slice($this->historyLines, -$this->historyLimit, $this->historyLimit); + } + + return $this; + } + + /** + * set autocompletion handler to use + * + * The autocomplete handler will be called whenever the user hits the TAB + * key. + * + * @param callable|null $autocomplete + * @return self + * @throws \InvalidArgumentException if the given callable is invalid + * @deprecated use Stdio::setAutocomplete() instead + */ + public function setAutocomplete($autocomplete) + { + if ($autocomplete !== null && !is_callable($autocomplete)) { + throw new \InvalidArgumentException('Invalid autocomplete function given'); + } + + $this->autocomplete = $autocomplete; + + return $this; + } + + /** + * Whether or not to emit a audible/visible BELL signal when using a disabled function + * + * By default, this class will emit a BELL signal when using a disable function, + * such as using the <kbd>left</kbd> or <kbd>backspace</kbd> keys when + * already at the beginning of the line. + * + * Whether or not the BELL is audible/visible depends on the termin and its + * settings, i.e. some terminals may "beep" or flash the screen or emit a + * short vibration. + * + * @param bool $bell + * @return void + * @internal use Stdio::setBell() instead + */ + public function setBell($bell) + { + $this->bell = (bool)$bell; + } + + /** + * redraw the current input prompt + * + * Usually, there should be no need to call this method manually. It will + * be invoked automatically whenever we detect the readline input needs to + * be (re)written to the output. + * + * Clear the current line and draw the input prompt. If input echo is + * enabled, will also draw the current input buffer and move to the current + * input buffer position. + * + * @return self + * @internal + */ + public function redraw() + { + // Erase characters from cursor to end of line and then redraw actual input + $this->output->write("\r\033[K" . $this->getDrawString()); + + return $this; + } + + /** + * Returns the string that is used to draw the input prompt + * + * @return string + * @internal + */ + public function getDrawString() + { + $output = $this->prompt; + if ($this->echo !== false) { + if ($this->echo === true) { + $buffer = $this->linebuffer; + } else { + $buffer = str_repeat($this->echo, $this->strlen($this->linebuffer)); + } + + // write output, then move back $reverse chars (by sending backspace) + $output .= $buffer . str_repeat("\x08", $this->strwidth($buffer) - $this->getCursorCell()); + } + + return $output; + } + + /** @internal */ + public function onKeyBackspace() + { + // left delete only if not at the beginning + if ($this->linepos === 0) { + $this->bell(); + } else { + $this->deleteChar($this->linepos - 1); + } + } + + /** @internal */ + public function onKeyDelete() + { + // right delete only if not at the end + if ($this->isEol()) { + $this->bell(); + } else { + $this->deleteChar($this->linepos); + } + } + + /** @internal */ + public function onKeyHome() + { + if ($this->move && $this->linepos !== 0) { + $this->moveCursorTo(0); + } else { + $this->bell(); + } + } + + /** @internal */ + public function onKeyEnd() + { + if ($this->move && !$this->isEol()) { + $this->moveCursorTo($this->strlen($this->linebuffer)); + } else { + $this->bell(); + } + } + + /** @internal */ + public function onKeyTab() + { + if ($this->autocomplete === null) { + $this->bell(); + return; + } + + // current word prefix and offset for start of word in input buffer + // "echo foo|bar world" will return just "foo" with word offset 5 + $word = $this->substr($this->linebuffer, 0, $this->linepos); + $start = 0; + $end = $this->linepos; + + // buffer prefix and postfix for everything that will *not* be matched + // above example will return "echo " and "bar world" + $prefix = ''; + $postfix = $this->substr($this->linebuffer, $this->linepos); + + // skip everything before last space + $pos = strrpos($word, ' '); + if ($pos !== false) { + $prefix = (string)substr($word, 0, $pos + 1); + $word = (string)substr($word, $pos + 1); + $start = $this->strlen($prefix); + } + + // skip double quote (") or single quote (') from argument + $quote = null; + if (isset($word[0]) && ($word[0] === '"' || $word[0] === '\'')) { + $quote = $word[0]; + ++$start; + $prefix .= $word[0]; + $word = (string)substr($word, 1); + } + + // invoke autocomplete callback + $words = call_user_func($this->autocomplete, $word, $start, $end); + + // return early if autocomplete does not return anything + if ($words === null) { + return; + } + + // remove from list of possible words that do not start with $word or are duplicates + $words = array_unique($words); + if ($word !== '' && $words) { + $words = array_filter($words, function ($w) use ($word) { + return strpos($w, $word) === 0; + }); + } + + // return if neither of the possible words match + if (!$words) { + $this->bell(); + return; + } + + // search longest common prefix among all possible matches + $found = reset($words); + $all = count($words); + if ($all > 1) { + while ($found !== '') { + // count all words that start with $found + $matches = count(array_filter($words, function ($w) use ($found) { + return strpos($w, $found) === 0; + })); + + // ALL words match $found => common substring found + if ($all === $matches) { + break; + } + + // remove last letter from $found and try again + $found = $this->substr($found, 0, -1); + } + + // found more than one possible match with this prefix => print options + if ($found === $word || $found === '') { + // limit number of possible matches + if (count($words) > $this->autocompleteSuggestions) { + $more = count($words) - ($this->autocompleteSuggestions - 1); + $words = array_slice($words, 0, $this->autocompleteSuggestions - 1); + $words []= '(+' . $more . ' others)'; + } + + $this->output->write("\n" . implode(' ', $words) . "\n"); + $this->redraw(); + + return; + } + } + + if ($quote !== null && $all === 1 && (strpos($postfix, $quote) === false || strpos($postfix, $quote) > strpos($postfix, ' '))) { + // add closing quote if word started in quotes and postfix does not already contain closing quote before next space + $found .= $quote; + } elseif ($found === '') { + // add single quotes around empty match + $found = '\'\''; + } + + if ($postfix === '' && $all === 1) { + // append single space after match unless there's a postfix or there are multiple completions + $found .= ' '; + } + + // replace word in input with best match and adjust cursor + $this->linebuffer = $prefix . $found . $postfix; + $this->moveCursorBy($this->strlen($found) - $this->strlen($word)); + } + + /** @internal */ + public function onKeyEnter() + { + if ($this->echo !== false) { + $this->output->write("\n"); + } + $this->processLine("\n"); + } + + /** @internal */ + public function onKeyLeft() + { + if ($this->move && $this->linepos !== 0) { + $this->moveCursorBy(-1); + } else { + $this->bell(); + } + } + + /** @internal */ + public function onKeyRight() + { + if ($this->move && !$this->isEol()) { + $this->moveCursorBy(1); + } else { + $this->bell(); + } + } + + /** @internal */ + public function onKeyUp() + { + // ignore if already at top or history is empty + if ($this->historyPosition === 0 || !$this->historyLines) { + $this->bell(); + return; + } + + if ($this->historyPosition === null) { + // first time up => move to last entry + $this->historyPosition = count($this->historyLines) - 1; + $this->historyUnsaved = $this->getInput(); + } else { + // somewhere in the list => move by one + $this->historyPosition--; + } + + $this->setInput($this->historyLines[$this->historyPosition]); + } + + /** @internal */ + public function onKeyDown() + { + // ignore if not currently cycling through history + if ($this->historyPosition === null) { + $this->bell(); + return; + } + + if (isset($this->historyLines[$this->historyPosition + 1])) { + // this is still a valid position => advance by one and apply + $this->historyPosition++; + $this->setInput($this->historyLines[$this->historyPosition]); + } else { + // moved beyond bottom => restore original unsaved input + $this->setInput($this->historyUnsaved); + $this->historyPosition = null; + $this->historyUnsaved = null; + } + } + + /** + * Will be invoked for character(s) that could not otherwise be processed by the sequencer + * + * @internal + */ + public function onFallback($chars, EventEmitterInterface $base = null) + { + // check if there's any special key binding for any of the chars + $buffer = ''; + foreach ($this->strsplit($chars) as $char) { + // forward compatibility: check if any key binding exists on base Stdio instance + // deprecated: check if any key binding exists on this Readline instance + $emit = null; + if ($base !== null && $base->listeners($char)) { + $emit = $base; + } else if ($this->listeners($char)) { + $emit = $this; + } + + if ($emit !== null) { + // special key binding for this character found + // process all characters before this one before invoking function + if ($buffer !== '') { + $this->addInput($buffer); + $buffer = ''; + } + $emit->emit($char, array($char)); + } else { + $buffer .= $char; + } + } + + // process remaining input characters after last special key binding + if ($buffer !== '') { + $this->addInput($buffer); + } + } + + /** + * delete a character at the given position + * + * Removing a character left to the current cursor will also move the cursor + * to the left. + * + * @param int $n + */ + private function deleteChar($n) + { + // read everything up until before current position + $pre = $this->substr($this->linebuffer, 0, $n); + $post = $this->substr($this->linebuffer, $n + 1); + + $this->linebuffer = $pre . $post; + + // move cursor one cell to the left if we're deleting in front of the cursor + if ($n < $this->linepos) { + --$this->linepos; + } + + $this->redraw(); + } + + /** + * process the current line buffer, emit event and redraw empty line + * + * @uses self::setInput() + */ + protected function processLine($eol) + { + // reset history cycle position + $this->historyPosition = null; + $this->historyUnsaved = null; + + // store and reset/clear/redraw current input + $line = $this->linebuffer; + if ($line !== '') { + // the line is not empty, reset it (and implicitly redraw prompt) + $this->setInput(''); + } elseif ($this->echo !== false) { + // explicitly redraw prompt after empty line + $this->redraw(); + } + + // process stored input buffer + $this->emit('data', array($line . $eol)); + } + + /** + * @param string $str + * @return int + * @codeCoverageIgnore + */ + private function strlen($str) + { + // prefer mb_strlen() if available + if (function_exists('mb_strlen')) { + return mb_strlen($str, $this->encoding); + } + + // otherwise replace all unicode chars with dots and count dots + return strlen(preg_replace('/./us', '.', $str)); + } + + /** + * @param string $str + * @param int $start + * @param ?int $len + * @return string + * @codeCoverageIgnore + */ + private function substr($str, $start = 0, $len = null) + { + if ($len === null) { + $len = $this->strlen($str) - $start; + } + + // prefer mb_substr() if available + if (function_exists('mb_substr')) { + return (string)mb_substr($str, $start, $len, $this->encoding); + } + + // otherwise build array with all unicode chars and slice array + preg_match_all('/./us', $str, $matches); + + return implode('', array_slice($matches[0], $start, $len)); + } + + /** + * @internal + * @param string $str + * @return int + * @codeCoverageIgnore + */ + public function strwidth($str) + { + // prefer mb_strwidth() if available + if (function_exists('mb_strwidth')) { + return mb_strwidth($str, $this->encoding); + } + + // otherwise replace each double-width unicode graphemes with two dots, all others with single dot and count number of dots + // mbstring's list of double-width graphemes is *very* long: https://3v4l.org/GEg3u + // let's use symfony's list from https://github.com/symfony/polyfill-mbstring/blob/e79d363049d1c2128f133a2667e4f4190904f7f4/Mbstring.php#L523 + // which looks like they originally came from http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c + return strlen(preg_replace( + array( + '/[\x{1100}-\x{115F}\x{2329}\x{232A}\x{2E80}-\x{303E}\x{3040}-\x{A4CF}\x{AC00}-\x{D7A3}\x{F900}-\x{FAFF}\x{FE10}-\x{FE19}\x{FE30}-\x{FE6F}\x{FF00}-\x{FF60}\x{FFE0}-\x{FFE6}\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}]/u', + '/./us', + ), + array( + '..', + '.', + ), + $str + )); + } + + /** + * @param string $str + * @return string[] + */ + private function strsplit($str) + { + return preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY); + } + + /** + * @return bool + */ + private function isEol() + { + return $this->linepos === $this->strlen($this->linebuffer); + } + + /** + * @return void + */ + private function bell() + { + if ($this->bell) { + $this->output->write("\x07"); // BEL a.k.a. \a + } + } + + /** @internal */ + public function handleEnd() + { + if ($this->linebuffer !== '') { + $this->processLine(''); + } + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** @internal */ + public function handleError(\Exception $error) + { + $this->emit('error', array($error)); + $this->close(); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } +} diff --git a/vendor/clue/stdio-react/src/Stdio.php b/vendor/clue/stdio-react/src/Stdio.php new file mode 100644 index 0000000..aff2959 --- /dev/null +++ b/vendor/clue/stdio-react/src/Stdio.php @@ -0,0 +1,630 @@ +<?php + +namespace Clue\React\Stdio; + +use Evenement\EventEmitter; +use React\EventLoop\LoopInterface; +use React\Stream\DuplexStreamInterface; +use React\Stream\ReadableResourceStream; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableResourceStream; +use React\Stream\WritableStreamInterface; + +class Stdio extends EventEmitter implements DuplexStreamInterface +{ + private $input; + private $output; + private $readline; + + private $ending = false; + private $closed = false; + private $incompleteLine = ''; + private $originalTtyMode = null; + + /** + * + * 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 ?ReadableStreamInterface $input + * @param ?WritableStreamInterface $output + * @param ?Readline $readline + */ + public function __construct(LoopInterface $loop = null, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null) + { + if ($input === null) { + $input = $this->createStdin($loop); // @codeCoverageIgnore + } + + if ($output === null) { + $output = $this->createStdout($loop); // @codeCoverageIgnore + } + + if ($readline === null) { + $readline = new Readline($input, $output, $this); + } + + $this->input = $input; + $this->output = $output; + $this->readline = $readline; + + $that = $this; + + // readline data emits a new line + $incomplete =& $this->incompleteLine; + $this->readline->on('data', function($line) use ($that, &$incomplete) { + // readline emits a new line on enter, so start with a blank line + $incomplete = ''; + $that->emit('data', array($line)); + }); + + // handle all input events (readline forwards all input events) + $this->readline->on('error', array($this, 'handleError')); + $this->readline->on('end', array($this, 'handleEnd')); + $this->readline->on('close', array($this, 'handleCloseInput')); + + // handle all output events + $this->output->on('error', array($this, 'handleError')); + $this->output->on('close', array($this, 'handleCloseOutput')); + } + + public function __destruct() + { + $this->restoreTtyMode(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function isWritable() + { + return $this->output->isWritable(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function write($data) + { + // return false if already ended, return true if writing empty string + if ($this->ending || $data === '') { + return !$this->ending; + } + + $out = $data; + + $lastNewline = strrpos($data, "\n"); + + $restoreReadline = false; + + if ($this->incompleteLine !== '') { + // the last write did not end with a newline => append to existing row + + // move one line up and move cursor to last position before writing data + $out = "\033[A" . "\r\033[" . $this->width($this->incompleteLine) . "C" . $out; + + // data contains a newline, so this will overwrite the readline prompt + if ($lastNewline !== false) { + // move cursor to beginning of readline prompt and clear line + // clearing is important because $data may not overwrite the whole line + $out = "\r\033[K" . $out; + + // make sure to restore readline after this output + $restoreReadline = true; + } + } else { + // here, we're writing to a new line => overwrite readline prompt + + // move cursor to beginning of readline prompt and clear line + $out = "\r\033[K" . $out; + + // we always overwrite the readline prompt, so restore it on next line + $restoreReadline = true; + } + + // following write will have have to append to this line if it does not end with a newline + $endsWithNewline = substr($data, -1) === "\n"; + + if ($endsWithNewline) { + // line ends with newline, so this is line is considered complete + $this->incompleteLine = ''; + } else { + // always end data with newline in order to append readline on next line + $out .= "\n"; + + if ($lastNewline === false) { + // contains no newline at all, everything is incomplete + $this->incompleteLine .= $data; + } else { + // contains a newline, everything behind it is incomplete + $this->incompleteLine = (string)substr($data, $lastNewline + 1); + } + } + + if ($restoreReadline) { + // write output and restore original readline prompt and line buffer + return $this->output->write($out . $this->readline->getDrawString()); + } else { + // restore original cursor position in readline prompt + $pos = $this->width($this->readline->getPrompt()) + $this->readline->getCursorCell(); + if ($pos !== 0) { + // we always start at beginning of line, move right by X + $out .= "\033[" . $pos . "C"; + } + + // write to actual output stream + return $this->output->write($out); + } + } + + public function end($data = null) + { + if ($this->ending) { + return; + } + + if ($data !== null) { + $this->write($data); + } + + $this->ending = true; + + // clear readline output, close input and end output + $this->readline->setInput('')->setPrompt(''); + $this->input->close(); + $this->output->end(); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->ending = true; + $this->closed = true; + + $this->input->close(); + $this->output->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** + * @deprecated + * @return Readline + */ + public function getReadline() + { + return $this->readline; + } + + + /** + * prompt to prepend to input line + * + * Will redraw the current input prompt with the current input buffer. + * + * @param string $prompt + * @return void + */ + public function setPrompt($prompt) + { + $this->readline->setPrompt($prompt); + } + + /** + * returns the prompt to prepend to input line + * + * @return string + * @see self::setPrompt() + */ + public function getPrompt() + { + return $this->readline->getPrompt(); + } + + /** + * sets whether/how to echo text input + * + * The default setting is `true`, which means that every character will be + * echo'ed as-is, i.e. you can see what you're typing. + * For example: Typing "test" shows "test". + * + * You can turn this off by supplying `false`, which means that *nothing* + * will be echo'ed while you're typing. This could be a good idea for + * password prompts. Note that this could be confusing for users, so using + * a character replacement as following is often preferred. + * For example: Typing "test" shows "" (nothing). + * + * Alternative, you can supply a single character replacement character + * that will be echo'ed for each character in the text input. This could + * be a good idea for password prompts, where an asterisk character ("*") + * is often used to indicate typing activity and password length. + * For example: Typing "test" shows "****" (with asterisk replacement) + * + * Changing this setting will redraw the current prompt and echo the current + * input buffer according to the new setting. + * + * @param boolean|string $echo echo can be turned on (boolean true) or off (boolean true), or you can supply a single character replacement string + * @return void + */ + public function setEcho($echo) + { + $this->readline->setEcho($echo); + } + + /** + * whether or not to support moving cursor left and right + * + * switching cursor support moves the cursor to the end of the current + * input buffer (if any). + * + * @param boolean $move + * @return void + */ + public function setMove($move) + { + $this->readline->setMove($move); + } + + /** + * Gets current cursor position measured in number of text characters. + * + * Note that the number of text characters doesn't necessarily reflect the + * number of monospace cells occupied by the text characters. If you want + * to know the latter, use `self::getCursorCell()` instead. + * + * @return int + * @see self::getCursorCell() to get the position measured in monospace cells + * @see self::moveCursorTo() to move the cursor to a given character position + * @see self::moveCursorBy() to move the cursor by given number of characters + * @see self::setMove() to toggle whether the user can move the cursor position + */ + public function getCursorPosition() + { + return $this->readline->getCursorPosition(); + } + + /** + * Gets current cursor position measured in monospace cells. + * + * Note that the cell position doesn't necessarily reflect the number of + * text characters. If you want to know the latter, use + * `self::getCursorPosition()` instead. + * + * Most "normal" characters occupy a single monospace cell, i.e. the ASCII + * sequence for "A" requires a single cell, as do most UTF-8 sequences + * like "Ä". + * + * However, there are a number of code points that do not require a cell + * (i.e. invisible surrogates) or require two cells (e.g. some asian glyphs). + * + * Also note that this takes the echo mode into account, i.e. the cursor is + * always at position zero if echo is off. If using a custom echo character + * (like asterisk), it will take its width into account instead of the actual + * input characters. + * + * @return int + * @see self::getCursorPosition() to get current cursor position measured in characters + * @see self::moveCursorTo() to move the cursor to a given character position + * @see self::moveCursorBy() to move the cursor by given number of characters + * @see self::setMove() to toggle whether the user can move the cursor position + * @see self::setEcho() + */ + public function getCursorCell() + { + return $this->readline->getCursorCell(); + } + + /** + * Moves cursor to right by $n chars (or left if $n is negative). + * + * Zero value or values out of range (exceeding current input buffer) are + * simply ignored. + * + * Will redraw() the readline only if the visible cell position changes, + * see `self::getCursorCell()` for more details. + * + * @param int $n + * @return void + */ + public function moveCursorBy($n) + { + $this->readline->moveCursorBy($n); + } + + /** + * Moves cursor to given position in current line buffer. + * + * Values out of range (exceeding current input buffer) are simply ignored. + * + * Will redraw() the readline only if the visible cell position changes, + * see `self::getCursorCell()` for more details. + * + * @param int $n + * @return void + */ + public function moveCursorTo($n) + { + $this->readline->moveCursorTo($n); + } + + /** + * Appends the given input to the current text input buffer at the current position + * + * This moves the cursor accordingly to the number of characters added. + * + * @param string $input + * @return void + */ + public function addInput($input) + { + $this->readline->addInput($input); + } + + /** + * set current text input buffer + * + * this moves the cursor to the end of the current + * input buffer (if any). + * + * @param string $input + * @return void + */ + public function setInput($input) + { + $this->readline->setInput($input); + } + + /** + * get current text input buffer + * + * @return string + */ + public function getInput() + { + return $this->readline->getInput(); + } + + /** + * Adds a new line to the (bottom position of the) history list + * + * @param string $line + * @return void + */ + public function addHistory($line) + { + $this->readline->addHistory($line); + } + + /** + * Clears the complete history list + * + * @return void + */ + public function clearHistory() + { + $this->readline->clearHistory(); + } + + /** + * Returns an array with all lines in the history + * + * @return string[] + */ + public function listHistory() + { + return $this->readline->listHistory(); + } + + /** + * Limits the history to a maximum of N entries and truncates the current history list accordingly + * + * @param int|null $limit + * @return void + */ + public function limitHistory($limit) + { + $this->readline->limitHistory($limit); + } + + /** + * set autocompletion handler to use + * + * The autocomplete handler will be called whenever the user hits the TAB + * key. + * + * @param callable|null $autocomplete + * @return void + * @throws \InvalidArgumentException if the given callable is invalid + */ + public function setAutocomplete($autocomplete) + { + $this->readline->setAutocomplete($autocomplete); + } + + /** + * whether or not to emit a audible/visible BELL signal when using a disabled function + * + * By default, this class will emit a BELL signal when using a disable function, + * such as using the <kbd>left</kbd> or <kbd>backspace</kbd> keys when + * already at the beginning of the line. + * + * Whether or not the BELL is audible/visible depends on the termin and its + * settings, i.e. some terminals may "beep" or flash the screen or emit a + * short vibration. + * + * @param bool $bell + * @return void + */ + public function setBell($bell) + { + $this->readline->setBell($bell); + } + + private function width($str) + { + return $this->readline->strwidth($str) - 2 * substr_count($str, "\x08"); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('end'); + } + + /** @internal */ + public function handleCloseInput() + { + $this->restoreTtyMode(); + + if (!$this->output->isWritable()) { + $this->close(); + } + } + + /** @internal */ + public function handleCloseOutput() + { + if (!$this->input->isReadable()) { + $this->close(); + } + } + + /** + * @codeCoverageIgnore this is covered by functional tests with/without ext-readline + */ + private function restoreTtyMode() + { + if (function_exists('readline_callback_handler_remove')) { + // remove dummy readline handler to turn to default input mode + readline_callback_handler_remove(); + } elseif ($this->originalTtyMode !== null && is_resource(STDIN) && $this->isTty()) { + // Reset stty so it behaves normally again + shell_exec('stty ' . escapeshellarg($this->originalTtyMode)); + $this->originalTtyMode = null; + } + + // restore blocking mode so following programs behave normally + if (defined('STDIN') && is_resource(STDIN)) { + stream_set_blocking(STDIN, true); + } + } + + /** + * @param ?LoopInterface $loop + * @return ReadableStreamInterface + * @codeCoverageIgnore this is covered by functional tests with/without ext-readline + */ + private function createStdin(LoopInterface $loop = null) + { + // STDIN not defined ("php -a") or already closed (`fclose(STDIN)`) + // also support starting program with closed STDIN ("example.php 0<&-") + // the stream is a valid resource and is not EOF, but fstat fails + if (!defined('STDIN') || !is_resource(STDIN) || fstat(STDIN) === false) { + $stream = new ReadableResourceStream(fopen('php://memory', 'r'), $loop); + $stream->close(); + return $stream; + } + + $stream = new ReadableResourceStream(STDIN, $loop); + + if (function_exists('readline_callback_handler_install')) { + // Prefer `ext-readline` to install dummy handler to turn on raw input mode. + // We will never actually feed the readline handler and instead + // handle all input in our `Readline` implementation. + readline_callback_handler_install('', function () { }); + return $stream; + } + + if ($this->isTty()) { + $this->originalTtyMode = rtrim(shell_exec('stty -g'), PHP_EOL); + + // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) + shell_exec('stty -icanon -echo'); + } + + // register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception) + // this will not trigger on SIGKILL etc., but the terminal should take care of this + register_shutdown_function(array($this, 'close')); + + return $stream; + } + + /** + * @param ?LoopInterface $loop + * @return WritableStreamInterface + * @codeCoverageIgnore this is covered by functional tests + */ + private function createStdout(LoopInterface $loop = null) + { + // STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`) + // also support starting program with closed STDOUT ("example.php >&-") + // the stream is a valid resource and is not EOF, but fstat fails + if (!defined('STDOUT') || !is_resource(STDOUT) || fstat(STDOUT) === false) { + $output = new WritableResourceStream(fopen('php://memory', 'r+'), $loop); + $output->close(); + } else { + $output = new WritableResourceStream(STDOUT, $loop); + } + + return $output; + } + + /** + * @return bool + * @codeCoverageIgnore + */ + private function isTty() + { + if (PHP_VERSION_ID >= 70200) { + // Prefer `stream_isatty()` (available as of PHP 7.2 only) + return stream_isatty(STDIN); + } elseif (function_exists('posix_isatty')) { + // Otherwise use `posix_isatty` if available (requires `ext-posix`) + return posix_isatty(STDIN); + } + + // otherwise try to guess based on stat file mode and device major number + // Must be special character device: ($mode & S_IFMT) === S_IFCHR + // And device major number must be allocated to TTYs (2-5 and 128-143) + // For what it's worth, checking for device gid 5 (tty) is less reliable. + // @link http://man7.org/linux/man-pages/man7/inode.7.html + // @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices + $stat = fstat(STDIN); + $mode = isset($stat['mode']) ? ($stat['mode'] & 0170000) : 0; + $major = isset($stat['dev']) ? (($stat['dev'] >> 8) & 0xff) : 0; + + return ($mode === 0020000 && $major >= 2 && $major <= 143 && ($major <=5 || $major >= 128)); + } +} diff --git a/vendor/clue/term-react/CHANGELOG.md b/vendor/clue/term-react/CHANGELOG.md new file mode 100644 index 0000000..b3d521d --- /dev/null +++ b/vendor/clue/term-react/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +## 1.3.0 (2020-11-06) + +* Improve test suite and add `.gitattributes` to exclude dev files from export. + Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. + (#29 and #30 by @SimonFrings) + +## 1.2.0 (2018-07-09) + +* Feature: Forward compatiblity with EventLoop v0.5 and upcoming v1.0. + (#28 by @clue) + +* Improve test suite by updating Travis config to test against legacy PHP 5.3 through PHP 7.2. + (#27 by @clue) + +* Update project homepage. + (#26 by @clue) + +## 1.1.0 (2017-07-06) + +* Feature: Forward compatibility with Stream v1.0 and v0.7 (while keeping BC) + (#22 by @Yoshi2889 and #23 by @WyriHaximus) + +* Improve test suite by fixing HHVM builds and ignoring future errors + (#24 by @clue) + +## 1.0.0 (2017-04-06) + +* First stable release, now following SemVer + + > Contains no other changes, so it's actually fully compatible with the v0.1 releases. + +## 0.1.3 (2017-04-06) + +* Feature: Forward compatibility with Stream v0.6 and v0.5 (while keeping BC) + (#18 and #20 by @clue) + +* Improve test suite by adding PHPUnit to require-dev + (#19 by @clue) + +## 0.1.2 (2016-06-14) + +* Fix: Fix processing events when input writes during data event + (#15 by @clue) + +* Fix: Stop emitting events when closing stream during event handler + (#16 by @clue) + +* Fix: Remove all event listeners when either stream closes + (#17 by @clue) + +## 0.1.1 (2016-06-13) + +* Fix: Continue parsing after a `c0` code in the middle of a stream + (#13 by @clue) + +* Add more examples + (#14 by @clue) + +## 0.1.0 (2016-06-03) + +* First tagged release diff --git a/vendor/clue/term-react/LICENSE b/vendor/clue/term-react/LICENSE new file mode 100644 index 0000000..7baae8e --- /dev/null +++ b/vendor/clue/term-react/LICENSE @@ -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. diff --git a/vendor/clue/term-react/README.md b/vendor/clue/term-react/README.md new file mode 100644 index 0000000..1768474 --- /dev/null +++ b/vendor/clue/term-react/README.md @@ -0,0 +1,171 @@ +# clue/reactphp-term [![Build Status](https://travis-ci.org/clue/reactphp-term.svg?branch=master)](https://travis-ci.org/clue/reactphp-term) + +Streaming terminal emulator, built on top of [ReactPHP](https://reactphp.org/). + +**Table of Contents** + +* [Support us](#support-us) +* [Usage](#usage) + * [ControlCodeParser](#controlcodeparser) +* [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! 🚀 + +## Usage + +### ControlCodeParser + +The `ControlCodeParser(ReadableStreamInterface $input)` class can be used to +parse any control code byte sequences (ANSI / VT100) when reading from an input stream and it +only returns its plain data stream. +It wraps a given `ReadableStreamInterface` and exposes its plain data through +the same interface. + +```php +$stdin = new ReadableResourceStream(STDIN, $loop); + +$stream = new ControlCodeParser($stdin); + +$stream->on('data', function ($chunk) { + var_dump($chunk); +}); +``` + +As such, you can be sure the resulting `data` events never include any control +code byte sequences and it can be processed like a normal plain data stream. + +React's streams emit chunks of data strings and make no assumption about any +byte sequences. +These chunks do not necessarily represent complete control code byte sequences, +as a sequence may be broken up into multiple chunks. +This class reassembles these sequences by buffering incomplete ones. + +The following [C1 control codes](https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_set) +are supported as defined in [ISO/IEC 2022](https://en.wikipedia.org/wiki/ISO/IEC_2022): + +* [CSI (Control Sequence Introducer)](https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_codes) + is one of the most common forms of control code sequences. + For example, CSI is used to print colored console output, also known as + "ANSI color codes" or the more technical term + [SGR (Select Graphic Rendition)](https://en.wikipedia.org/wiki/ANSI_escape_code#graphics). + CSI codes also appear on `STDIN`, for example when the user hits special keys, + such as the cursor, `HOME`, `END` etc. keys. + +* OSC (Operating System Command) + is another common form of control code sequences. + For example, OSC is used to change the window title or window icon. + +* APC (Application Program-Control) + +* DPS (Device-Control string) + +* PM (Privacy Message) + +Each code sequence gets emitted with a dedicated event with its raw byte sequence: + +```php +$stream->on('csi', function ($sequence) { + if ($sequence === "\x1B[A") { + echo 'cursor UP pressed'; + } else if ($sequence === "\x1B[B") { + echo 'cursor DOWN pressed'; + } +}); + +$stream->on('osc', function ($sequence) { … }); +$stream->on('apc', function ($sequence) { … }); +$stream->on('dps', function ($sequence) { … }); +$stream->on('pm', function ($sequence) { … }); +``` + +Other lesser known [C1 control codes](https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C1_set) +not listed above are supported by just emitting their 2-byte sequence. +Each generic C1 code gets emitted as an `c1` event with its raw 2-byte sequence: + +```php +$stream->on('c1', function ($sequence) { … }); +``` + +All other [C0 control codes](https://en.wikipedia.org/wiki/C0_and_C1_control_codes#C0_.28ASCII_and_derivatives.29), +also known as [ASCII control codes](https://en.wikipedia.org/wiki/ASCII#ASCII_control_code_chart), +are supported by just emitting their single-byte value. +Each generic C0 code gets emitted as an `c0` event with its raw single-byte value: + +```php +$stream->on('c0', function ($code) { + if ($code === "\n") { + echo 'ENTER pressed'; + } +}); +``` + +## 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/term-react:^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 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 +``` + +## 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 processing streams of data, refer to the documentation of + the underlying [react/stream](https://github.com/reactphp/stream) component. + +* If you want to process UTF-8 encoded console input, you may + want to use [clue/reactphp-utf8](https://github.com/clue/reactphp-utf8) on the resulting + plain data stream. + +* If you want to to display or inspect the control codes, you may + want to use either [clue/hexdump](https://github.com/clue/php-hexdump) or + [clue/caret-notation](https://github.com/clue/php-caret-notation) on the emitted + control byte sequences. + +* If you want to process standard input and output (STDIN and STDOUT) from a TTY, you may + want to use [clue/reactphp-stdio](https://github.com/clue/reactphp-stdio) instead of + using this low-level library. diff --git a/vendor/clue/term-react/composer.json b/vendor/clue/term-react/composer.json new file mode 100644 index 0000000..d1b3ce5 --- /dev/null +++ b/vendor/clue/term-react/composer.json @@ -0,0 +1,27 @@ +{ + "name": "clue/term-react", + "description": "Streaming terminal emulator, built on top of ReactPHP.", + "keywords": ["terminal", "control codes", "xterm", "ANSI", "ASCII", "VT100", "csi", "osc", "apc", "dps", "pm", "C1", "C0", "streaming", "ReactPHP"], + "homepage": "https://github.com/clue/reactphp-term", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "autoload": { + "psr-4": { "Clue\\React\\Term\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Term\\": "tests/" } + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.0 || ^0.7" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3" + } +} diff --git a/vendor/clue/term-react/src/ControlCodeParser.php b/vendor/clue/term-react/src/ControlCodeParser.php new file mode 100644 index 0000000..abbe400 --- /dev/null +++ b/vendor/clue/term-react/src/ControlCodeParser.php @@ -0,0 +1,223 @@ +<?php + +namespace Clue\React\Term; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; +use React\Stream\Util; + +class ControlCodeParser extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + private $buffer = ''; + + /** + * we know about the following C1 types (7 bit only) + * + * followed by "[" means it's CSI (Control Sequence Introducer) + * followed by "]" means it's OSC (Operating System Controls) + * followed by "_" means it's APC (Application Program-Control) + * followed by "P" means it's DPS (Device-Control string) + * followed by "^" means it's PM (Privacy Message) + * + * Each of these will be parsed until the sequence ends and then emitted + * under their respective name. + * + * All other C1 types will be emitted under the "c1" name without any + * further processing. + * + * C1 types in 8 bit are currently not supported, as they require special + * care with regards to whether UTF-8 mode is enabled. So far this has + * turned out to be a non-issue because most terminal emulators *accept* + * boths formats, but usually *send* in 7 bit mode exclusively. + */ + private $types = array( + '[' => 'csi', + ']' => 'osc', + '_' => 'apc', + 'P' => 'dps', + '^' => 'pm', + ); + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + if (!$this->input->isReadable()) { + return $this->close(); + } + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->buffer = ''; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + while ($this->buffer !== '') { + // search for first control character (C0 and DEL) + $c0 = false; + for ($i = 0; isset($this->buffer[$i]); ++$i) { + $code = ord($this->buffer[$i]); + if ($code < 0x20 || $code === 0x7F) { + $c0 = $i; + break; + } + } + + // no C0 found, emit whole buffer as data + if ($c0 === false) { + $data = $this->buffer; + $this->buffer = ''; + + $this->emit('data', array($data)); + return; + } + + // C0 found somewhere inbetween, emit everything before C0 as data + if ($c0 !== 0) { + $data = substr($this->buffer, 0, $c0); + $this->buffer = substr($this->buffer, $c0); + + $this->emit('data', array($data)); + continue; + } + + // C0 is now at start of buffer + // check if this is a normal C0 code or an ESC (\x1B = \033) + // normal C0 will be emitted, ESC will be parsed further + if ($this->buffer[0] !== "\x1B") { + $data = $this->buffer[0]; + $this->buffer = (string)substr($this->buffer, 1); + + $this->emit('c0', array($data)); + continue; + } + + // check following byte to determine type + if (!isset($this->buffer[1])) { + // type currently unknown, wait for next data chunk + break; + } + + // if this is an unknown type, just emit as "c1" without further parsing + if (!isset($this->types[$this->buffer[1]])) { + $data = substr($this->buffer, 0, 2); + $this->buffer = (string)substr($this->buffer, 2); + + $this->emit('c1', array($data)); + continue; + } + + // this is known type, check for the sequence end + $type = $this->types[$this->buffer[1]]; + $found = false; + + if ($type === 'csi') { + // CSI is now at the start of the buffer, search final character + for ($i = 2; isset($this->buffer[$i]); ++$i) { + $code = ord($this->buffer[$i]); + + // final character between \x40-\x7E + if ($code >= 64 && $code <= 126) { + $data = substr($this->buffer, 0, $i + 1); + $this->buffer = (string)substr($this->buffer, $i + 1); + + $this->emit($type, array($data)); + $found = true; + break; + } + } + } else { + // all other types are terminated by ST + // only OSC can also be terminted by BEL (whichever comes first) + $st = strpos($this->buffer, "\x1B\\"); + $bel = ($type === 'osc') ? strpos($this->buffer, "\x07") : false; + + if ($st !== false && ($bel === false || $bel > $st)) { + // ST comes before BEL or no BEL found + $data = substr($this->buffer, 0, $st + 2); + $this->buffer = (string)substr($this->buffer, $st + 2); + + $this->emit($type, array($data)); + $found = true; + } elseif ($bel !== false) { + // BEL comes before ST or no ST found + $data = substr($this->buffer, 0, $bel + 1); + $this->buffer = (string)substr($this->buffer, $bel + 1); + + $this->emit($type, array($data)); + $found = true; + } + } + + // no final character found => wait for next data chunk + if (!$found) { + break; + } + } + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + if ($this->buffer === '') { + $this->emit('end'); + } else { + $this->emit('error', array(new \RuntimeException('Stream ended with incomplete control code sequence in buffer'))); + } + $this->close(); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } +} diff --git a/vendor/clue/utf8-react/CHANGELOG.md b/vendor/clue/utf8-react/CHANGELOG.md new file mode 100644 index 0000000..41cce80 --- /dev/null +++ b/vendor/clue/utf8-react/CHANGELOG.md @@ -0,0 +1,41 @@ +# Changelog + +## 1.2.0 (2020-11-06) + +* Improve test suite and add `.gitattributes` to exclude dev files from export. + Prepare PHP 8 support, update to PHPUnit 9 and simplify test matrix. + (#15 by clue and #21 and #22 by @SimonFrings) + +* Update project homepage. + (#14 by @clue) + +## 1.1.0 (2017-07-06) + +* Feature: Forward compatibility with Stream v1.0 and v0.7 (while keeping BC) + (#12 by @WyriHaximus) + +* Improve test suite by fixing HHVM builds and ignoring future errors + (#13 by @clue) + +## 1.0.0 (2017-04-07) + +* First stable release, now following SemVer + + > Contains no other changes, so it's actually fully compatible with the v0.1 releases. + +## 0.1.2 (2017-04-07) + +* Feature: Forward compatibility with Stream v0.6 and v0.5 (while keeping BC) + (#10 by @clue) + +* Improve test suite by adding PHPUnit to require-dev + (#9 by @thklein) + +## 0.1.1 (2016-06-24) + +* Fix: Remove event listeners once closed and do not emit any further events + (#8 by @clue) + +## 0.1.0 (2016-06-01) + +* First tagged release diff --git a/vendor/clue/utf8-react/LICENSE b/vendor/clue/utf8-react/LICENSE new file mode 100644 index 0000000..7baae8e --- /dev/null +++ b/vendor/clue/utf8-react/LICENSE @@ -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. diff --git a/vendor/clue/utf8-react/README.md b/vendor/clue/utf8-react/README.md new file mode 100644 index 0000000..f26061f --- /dev/null +++ b/vendor/clue/utf8-react/README.md @@ -0,0 +1,116 @@ +# clue/reactphp-utf8 [![Build Status](https://travis-ci.org/clue/reactphp-utf8.svg?branch=master)](https://travis-ci.org/clue/reactphp-utf8) + +Streaming UTF-8 parser, built on top of [ReactPHP](https://reactphp.org/). + +**Table of Contents** + +* [Support us](#support-us) +* [Usage](#usage) + * [Sequencer](#sequencer) +* [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! 🚀 + +## Usage + +### Sequencer + +The `Sequencer` class can be used to make sure you only get back complete, valid +UTF-8 byte sequences when reading from a stream. +It wraps a given `ReadableStreamInterface` and exposes its data through the same +interface. + +```php +$stdin = new ReadableResourceStream(STDIN, $loop); + +$stream = new Sequencer($stdin); + +$stream->on('data', function ($chunk) { + var_dump($chunk); +}); +``` + +React's streams emit chunks of data strings and make no assumption about its encoding. +These chunks do not necessarily represent complete UTF-8 byte sequences, as a +sequence may be broken up into multiple chunks. +This class reassembles these sequences by buffering incomplete ones. + +Also, if you're merely consuming a stream and you're not in control of producing and +ensuring valid UTF-8 data, it may as well include invalid UTF-8 byte sequences. +This class replaces any invalid bytes in the sequence with a `?`. +This replacement character can be given as a second paramter to the constructor: + +```php +$stream = new Sequencer($stdin, 'X'); +``` + +As such, you can be sure you never get an invalid UTF-8 byte sequence out of +the resulting stream. + +Note that the stream may still contain ASCII control characters or +ANSI / VT100 control byte sequences, as they're valid UTF-8. +This binary data will be left as-is, unless you filter this at a later stage. + +## 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/utf8-react:^1.2 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 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 +``` + +## 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 processing streams of data, refer to the documentation of + the underlying [react/stream](https://github.com/reactphp/stream) component. + +* If you want to process ASCII control characters or ANSI / VT100 control byte sequences, you may + want to use [clue/reactphp-term](https://github.com/clue/reactphp-term) on the raw input + stream before passing the resulting stream to the UTF-8 sequencer. + +* If you want to to display or inspect the byte sequences, you may + want to use [clue/hexdump](https://github.com/clue/php-hexdump) on the emitted byte sequences. diff --git a/vendor/clue/utf8-react/composer.json b/vendor/clue/utf8-react/composer.json new file mode 100644 index 0000000..931708f --- /dev/null +++ b/vendor/clue/utf8-react/composer.json @@ -0,0 +1,27 @@ +{ + "name": "clue/utf8-react", + "description": "Streaming UTF-8 parser, built on top of ReactPHP.", + "keywords": ["UTF-8", "utf8", "unicode", "streaming", "ReactPHP"], + "homepage": "https://github.com/clue/reactphp-utf8", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "autoload": { + "psr-4": { "Clue\\React\\Utf8\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Utf8\\": "tests/" } + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4 || ^0.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 ||^5.7 || ^4.8", + "react/stream": "^1.0 || ^0.7" + } +} diff --git a/vendor/clue/utf8-react/src/Sequencer.php b/vendor/clue/utf8-react/src/Sequencer.php new file mode 100644 index 0000000..e9bf433 --- /dev/null +++ b/vendor/clue/utf8-react/src/Sequencer.php @@ -0,0 +1,174 @@ +<?php + +namespace Clue\React\Utf8; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\WritableStreamInterface; +use React\Stream\Util; + +/** + * forwards only complete UTF-8 sequences + */ +class Sequencer extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $invalid; + + private $buffer = ''; + private $closed = false; + + public function __construct(ReadableStreamInterface $input, $replacementCharacter = '?') + { + $this->input = $input; + $this->invalid = $replacementCharacter; + + if (!$input->isReadable()) { + return $this->close(); + } + + $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')); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + $len = strlen($this->buffer); + + $sequence = ''; + $expect = 0; + $out = ''; + + for ($i = 0; $i < $len; ++$i) { + $char = $this->buffer[$i]; + $code = ord($char); + + if ($code & 128) { + // multi-byte sequence + if ($code & 64) { + // this is the start of a sequence + + // unexpected start of sequence because already within sequence + if ($expect !== 0) { + $out .= str_repeat($this->invalid, strlen($sequence)); + $sequence = ''; + } + + $sequence = $char; + $expect = 2; + + if ($code & 32) { + ++$expect; + if ($code & 16) { + ++$expect; + + if ($code & 8) { + // invalid sequence start length + $out .= $this->invalid; + $sequence = ''; + $expect = 0; + } + } + } + } else { + // this is a follow-up byte in a sequence + if ($expect === 0) { + // we're not within a sequence in first place + $out .= $this->invalid; + } else { + // valid following byte in sequence + $sequence .= $char; + + // sequence reached expected length => add to output + if (strlen($sequence) === $expect) { + $out .= $sequence; + $sequence = ''; + $expect = 0; + } + } + } + } else { + // simple ASCII character found + + // unexpected because already within sequence + if ($expect !== 0) { + $out .= str_repeat($this->invalid, strlen($sequence)); + $sequence = ''; + $expect = 0; + } + + $out .= $char; + } + } + + if ($out !== '') { + $this->buffer = substr($this->buffer, strlen($out)); + + $this->emit('data', array($out)); + } + } + + /** @internal */ + public function handleEnd() + { + if ($this->buffer !== '' && $this->invalid !== '') { + $data = str_repeat($this->invalid, strlen($this->buffer)); + $this->buffer = ''; + + $this->emit('data', array($data)); + } + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** @internal */ + public function handleError(\Exception $error) + { + $this->emit('error', array($error)); + $this->close(); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->buffer = ''; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + 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; + } +} |