diff options
Diffstat (limited to 'vendor/clue')
78 files changed, 9935 insertions, 0 deletions
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/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/buzz-react/LICENSE b/vendor/clue/buzz-react/LICENSE new file mode 100644 index 0000000..da15612 --- /dev/null +++ b/vendor/clue/buzz-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/buzz-react/composer.json b/vendor/clue/buzz-react/composer.json new file mode 100644 index 0000000..99e6d69 --- /dev/null +++ b/vendor/clue/buzz-react/composer.json @@ -0,0 +1,38 @@ +{ + "name": "clue/buzz-react", + "description": "Simple, async PSR-7 HTTP client for concurrently processing any number of HTTP requests, built on top of ReactPHP", + "keywords": ["HTTP client", "PSR-7", "HTTP", "ReactPHP", "async"], + "homepage": "https://github.com/clue/reactphp-buzz", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "autoload": { + "psr-4": { "Clue\\React\\Buzz\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Buzz\\": "tests/" } + }, + "require": { + "php": ">=5.3", + "psr/http-message": "^1.0", + "react/event-loop": "^1.0 || ^0.5", + "react/http-client": "^0.5.10", + "react/promise": "^2.2.1 || ^1.2.1", + "react/promise-stream": "^1.0 || ^0.1.2", + "react/socket": "^1.1", + "react/stream": "^1.0 || ^0.7", + "ringcentral/psr7": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.0", + "clue/http-proxy-react": "^1.3", + "clue/reactphp-ssh-proxy": "^1.0", + "clue/socks-react": "^1.0", + "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35", + "react/http": "^0.8" + } +} diff --git a/vendor/clue/buzz-react/src/Browser.php b/vendor/clue/buzz-react/src/Browser.php new file mode 100644 index 0000000..8f1a751 --- /dev/null +++ b/vendor/clue/buzz-react/src/Browser.php @@ -0,0 +1,867 @@ +<?php + +namespace Clue\React\Buzz; + +use Clue\React\Buzz\Io\Sender; +use Clue\React\Buzz\Io\Transaction; +use Clue\React\Buzz\Message\MessageFactory; +use InvalidArgumentException; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\UriInterface; +use React\EventLoop\LoopInterface; +use React\Promise\PromiseInterface; +use React\Socket\ConnectorInterface; +use React\Stream\ReadableStreamInterface; + +class Browser +{ + private $transaction; + private $messageFactory; + private $baseUrl; + private $protocolVersion = '1.1'; + + /** + * The `Browser` is responsible for sending HTTP requests to your HTTP server + * and keeps track of pending incoming HTTP responses. + * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). + * + * ```php + * $loop = React\EventLoop\Factory::create(); + * + * $browser = new Clue\React\Buzz\Browser($loop); + * ``` + * + * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, + * proxy servers etc.), you can explicitly pass a custom instance of the + * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + * + * ```php + * $connector = new React\Socket\Connector($loop, array( + * 'dns' => '127.0.0.1', + * 'tcp' => array( + * 'bindto' => '192.168.10.1:0' + * ), + * 'tls' => array( + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ) + * )); + * + * $browser = new Clue\React\Buzz\Browser($loop, $connector); + * ``` + * + * @param LoopInterface $loop + * @param ConnectorInterface|null $connector [optional] Connector to use. + * Should be `null` in order to use default Connector. + */ + public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) + { + $this->messageFactory = new MessageFactory(); + $this->transaction = new Transaction( + Sender::createFromLoop($loop, $connector, $this->messageFactory), + $this->messageFactory, + $loop + ); + } + + /** + * Sends an HTTP GET request + * + * ```php + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * See also [example 01](../examples/01-google.php). + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @return PromiseInterface<ResponseInterface> + */ + public function get($url, array $headers = array()) + { + return $this->requestMayBeStreaming('GET', $url, $headers); + } + + /** + * Sends an HTTP POST request + * + * ```php + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }); + * ``` + * + * See also [example 04](../examples/04-post-json.php). + * + * This method is also commonly used to submit HTML form data: + * + * ```php + * $data = [ + * 'user' => 'Alice', + * 'password' => 'secret' + * ]; + * + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/x-www-form-urlencoded' + * ], + * http_build_query($data) + * ); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->post($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface<ResponseInterface> + */ + public function post($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('POST', $url, $headers, $contents); + } + + /** + * Sends an HTTP HEAD request + * + * ```php + * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump($response->getHeaders()); + * }); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @return PromiseInterface<ResponseInterface> + */ + public function head($url, array $headers = array()) + { + return $this->requestMayBeStreaming('HEAD', $url, $headers); + } + + /** + * Sends an HTTP PATCH request + * + * ```php + * $browser->patch( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->patch($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface<ResponseInterface> + */ + public function patch($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); + } + + /** + * Sends an HTTP PUT request + * + * ```php + * $browser->put( + * $url, + * [ + * 'Content-Type' => 'text/xml' + * ], + * $xml->asXML() + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * See also [example 05](../examples/05-put-xml.php). + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->put($url, array('Content-Length' => '11'), $body); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface<ResponseInterface> + */ + public function put($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); + } + + /** + * Sends an HTTP DELETE request + * + * ```php + * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface<ResponseInterface> + */ + public function delete($url, array $headers = array(), $contents = '') + { + return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); + } + + /** + * Sends an arbitrary HTTP request. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * > Note that this method is available as of v2.9.0 and always buffers the + * response body before resolving. + * It does not respect the deprecated [`streaming` option](#withoptions). + * If you want to stream the response body, you can use the + * [`requestStreaming()`](#requeststreaming) method instead. + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface<ResponseInterface,Exception> + * @since 2.9.0 + */ + public function request($method, $url, array $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * In some situations, it's a better idea to use a streaming approach, where + * only small chunks have to be kept in memory. You can use this method to + * send an arbitrary HTTP request and receive a streaming response. It uses + * the same HTTP message API, but does not buffer the response body in + * memory. It only processes the response body in small chunks as data is + * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). + * This works for (any number of) responses of arbitrary sizes. + * + * ```php + * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * $body = $response->getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * $body->on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $body->on('error', function (Exception $error) { + * echo 'Error: ' . $error->getMessage() . PHP_EOL; + * }); + * + * $body->on('close', function () { + * echo '[DONE]' . PHP_EOL; + * }); + * }); + * ``` + * + * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + * and the [streaming response](#streaming-response) for more details, + * examples and possible use-cases. + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * $loop->addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * > Note that this method is available as of v2.9.0 and always resolves the + * response without buffering the response body. + * It does not respect the deprecated [`streaming` option](#withoptions). + * If you want to buffer the response body, use can use the + * [`request()`](#request) method instead. + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface<ResponseInterface,Exception> + * @since 2.9.0 + */ + public function requestStreaming($method, $url, $headers = array(), $contents = '') + { + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); + } + + /** + * [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). + * + * ```php + * // deprecated: see post() instead + * $browser->submit($url, array('user' => 'test', 'password' => 'secret')); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header for the encoded length of the given `$fields`. + * + * > For BC reasons, this method accepts the `$url` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * @param string|UriInterface $url URL for the request. + * @param array $fields + * @param array $headers + * @param string $method + * @return PromiseInterface<ResponseInterface> + * @deprecated 2.9.0 See self::post() instead. + * @see self::post() + */ + public function submit($url, array $fields, $headers = array(), $method = 'POST') + { + $headers['Content-Type'] = 'application/x-www-form-urlencoded'; + $contents = http_build_query($fields); + + return $this->requestMayBeStreaming($method, $url, $headers, $contents); + } + + /** + * [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $request = new Request('OPTIONS', $url); + * + * // deprecated: see request() instead + * $browser->send($request)->then(…); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * @param RequestInterface $request + * @return PromiseInterface<ResponseInterface> + * @deprecated 2.9.0 See self::request() instead. + * @see self::request() + */ + public function send(RequestInterface $request) + { + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); + } + + return $this->transaction->send($request); + } + + /** + * Changes the maximum timeout used for waiting for pending requests. + * + * You can pass in the number of seconds to use as a new timeout value: + * + * ```php + * $browser = $browser->withTimeout(10.0); + * ``` + * + * You can pass in a bool `false` to disable any timeouts. In this case, + * requests can stay pending forever: + * + * ```php + * $browser = $browser->withTimeout(false); + * ``` + * + * You can pass in a bool `true` to re-enable default timeout handling. This + * will respects PHP's `default_socket_timeout` setting (default 60s): + * + * ```php + * $browser = $browser->withTimeout(true); + * ``` + * + * See also [timeouts](#timeouts) for more details about timeout handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given timeout value applied. + * + * @param bool|number $timeout + * @return self + */ + public function withTimeout($timeout) + { + if ($timeout === true) { + $timeout = null; + } elseif ($timeout === false) { + $timeout = -1; + } elseif ($timeout < 0) { + $timeout = 0; + } + + return $this->withOptions(array( + 'timeout' => $timeout, + )); + } + + /** + * Changes how HTTP redirects will be followed. + * + * You can pass in the maximum number of redirects to follow: + * + * ```php + * $new = $browser->withFollowRedirects(5); + * ``` + * + * The request will automatically be rejected when the number of redirects + * is exceeded. You can pass in a `0` to reject the request for any + * redirects encountered: + * + * ```php + * $browser = $browser->withFollowRedirects(0); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // only non-redirected responses will now end up here + * var_dump($response->getHeaders()); + * }); + * ``` + * + * You can pass in a bool `false` to disable following any redirects. In + * this case, requests will resolve with the redirection response instead + * of following the `Location` response header: + * + * ```php + * $browser = $browser->withFollowRedirects(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any redirects will now end up here + * var_dump($response->getHeaderLine('Location')); + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default redirect handling. + * This defaults to following a maximum of 10 redirects: + * + * ```php + * $browser = $browser->withFollowRedirects(true); + * ``` + * + * See also [redirects](#redirects) for more details about redirect handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given redirect setting applied. + * + * @param bool|int $followRedirects + * @return self + */ + public function withFollowRedirects($followRedirects) + { + return $this->withOptions(array( + 'followRedirects' => $followRedirects !== false, + 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects + )); + } + + /** + * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + * + * You can pass in a bool `false` to disable rejecting incoming responses + * that use a 4xx or 5xx response status code. In this case, requests will + * resolve with the response message indicating an error condition: + * + * ```php + * $browser = $browser->withRejectErrorResponse(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default status code handling. + * This defaults to rejecting any response status codes in the 4xx or 5xx + * range: + * + * ```php + * $browser = $browser->withRejectErrorResponse(true); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any successful HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * if ($e instanceof Clue\React\Buzz\Message\ResponseException) { + * // any HTTP response error message will now end up here + * $response = $e->getResponse(); + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * } else { + * var_dump($e->getMessage()); + * } + * }); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param bool $obeySuccessCode + * @return self + */ + public function withRejectErrorResponse($obeySuccessCode) + { + return $this->withOptions(array( + 'obeySuccessCode' => $obeySuccessCode, + )); + } + + /** + * Changes the base URL used to resolve relative URLs to. + * + * If you configure a base URL, any requests to relative URLs will be + * processed by first prepending this absolute base URL. Note that this + * merely prepends the base URL and does *not* resolve any relative path + * references (like `../` etc.). This is mostly useful for (RESTful) API + * calls where all endpoints (URLs) are located under a common base URL. + * + * ```php + * $browser = $browser->withBase('http://api.example.com/v3'); + * + * // will request http://api.example.com/v3/example + * $browser->get('/example')->then(…); + * ``` + * + * You can pass in a `null` base URL to return a new instance that does not + * use a base URL: + * + * ```php + * $browser = $browser->withBase(null); + * ``` + * + * Accordingly, any requests using relative URLs to a browser that does not + * use a base URL can not be completed and will be rejected without sending + * a request. + * + * This method will throw an `InvalidArgumentException` if the given + * `$baseUrl` argument is not a valid URL. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method + * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + * + * > For BC reasons, this method accepts the `$baseUrl` as either a `string` + * value or as an `UriInterface`. It's recommended to explicitly cast any + * objects implementing `UriInterface` to `string`. + * + * > Changelog: As of v2.9.0 this method accepts a `null` value to reset the + * base URL. Earlier versions had to use the deprecated `withoutBase()` + * method to reset the base URL. + * + * @param string|null|UriInterface $baseUrl absolute base URL + * @return self + * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL + * @see self::withoutBase() + */ + public function withBase($baseUrl) + { + $browser = clone $this; + if ($baseUrl === null) { + $browser->baseUrl = null; + return $browser; + } + + $browser->baseUrl = $this->messageFactory->uri($baseUrl); + if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + throw new \InvalidArgumentException('Base URL must be absolute'); + } + + return $browser; + } + + /** + * Changes the HTTP protocol version that will be used for all subsequent requests. + * + * All the above [request methods](#request-methods) default to sending + * requests as HTTP/1.1. This is the preferred HTTP protocol version which + * also provides decent backwards-compatibility with legacy HTTP/1.0 + * servers. As such, there should rarely be a need to explicitly change this + * protocol version. + * + * If you want to explicitly use the legacy HTTP/1.0 protocol version, you + * can use this method: + * + * ```php + * $newBrowser = $browser->withProtocolVersion('1.0'); + * + * $newBrowser->get($url)->then(…); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * new protocol version applied. + * + * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" + * @return self + * @throws InvalidArgumentException + * @since 2.8.0 + */ + public function withProtocolVersion($protocolVersion) + { + if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); + } + + $browser = clone $this; + $browser->protocolVersion = (string) $protocolVersion; + + return $browser; + } + + /** + * Changes the maximum size for buffering a response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * By default, the response body buffer will be limited to 16 MiB. If the + * response body exceeds this maximum size, the request will be rejected. + * + * You can pass in the maximum number of bytes to buffer: + * + * ```php + * $browser = $browser->withResponseBuffer(1024 * 1024); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // response body will not exceed 1 MiB + * var_dump($response->getHeaders(), (string) $response->getBody()); + * }); + * ``` + * + * Note that the response body buffer has to be kept in memory for each + * pending request until its transfer is completed and it will only be freed + * after a pending request is fulfilled. As such, increasing this maximum + * buffer size to allow larger response bodies is usually not recommended. + * Instead, you can use the [`requestStreaming()` method](#requeststreaming) + * to receive responses with arbitrary sizes without buffering. Accordingly, + * this maximum buffer size setting has no effect on streaming responses. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param int $maximumSize + * @return self + * @see self::requestStreaming() + */ + public function withResponseBuffer($maximumSize) + { + return $this->withOptions(array( + 'maximumSize' => $maximumSize + )); + } + + /** + * [Deprecated] Changes the [options](#options) to use: + * + * The [`Browser`](#browser) class exposes several options for the handling of + * HTTP transactions. These options resemble some of PHP's + * [HTTP context options](http://php.net/manual/en/context.http.php) and + * can be controlled via the following API (and their defaults): + * + * ```php + * // deprecated + * $newBrowser = $browser->withOptions(array( + * 'timeout' => null, // see withTimeout() instead + * 'followRedirects' => true, // see withFollowRedirects() instead + * 'maxRedirects' => 10, // see withFollowRedirects() instead + * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + * 'streaming' => false, // deprecated, see requestStreaming() instead + * )); + * ``` + * + * See also [timeouts](#timeouts), [redirects](#redirects) and + * [streaming](#streaming) for more details. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * options applied. + * + * @param array $options + * @return self + * @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead. + * @see self::withTimeout() + * @see self::withFollowRedirects() + * @see self::withRejectErrorResponse() + */ + public function withOptions(array $options) + { + $browser = clone $this; + $browser->transaction = $this->transaction->withOptions($options); + + return $browser; + } + + /** + * [Deprecated] Removes the base URL. + * + * ```php + * // deprecated: see withBase() instead + * $newBrowser = $browser->withoutBase(); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method + * actually returns a *new* [`Browser`](#browser) instance without any base URL applied. + * + * See also [`withBase()`](#withbase). + * + * @return self + * @deprecated 2.9.0 See self::withBase() instead. + * @see self::withBase() + */ + public function withoutBase() + { + return $this->withBase(null); + } + + /** + * @param string $method + * @param string|UriInterface $url + * @param array $headers + * @param string|ReadableStreamInterface $contents + * @return PromiseInterface<ResponseInterface,Exception> + */ + private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') + { + return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); + } +} diff --git a/vendor/clue/buzz-react/src/Io/ChunkedEncoder.php b/vendor/clue/buzz-react/src/Io/ChunkedEncoder.php new file mode 100644 index 0000000..3b74e0c --- /dev/null +++ b/vendor/clue/buzz-react/src/Io/ChunkedEncoder.php @@ -0,0 +1,93 @@ +<?php + +namespace Clue\React\Buzz\Io; + +use Evenement\EventEmitter; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * [Internal] Encodes given payload stream with "Transfer-Encoding: chunked" and emits encoded data + * + * This is used internally to encode outgoing requests with this encoding. + * + * @internal + * @link https://github.com/reactphp/http/blob/master/src/Io/ChunkedEncoder.php Originally from react/http + */ +class ChunkedEncoder extends EventEmitter implements ReadableStreamInterface +{ + private $input; + private $closed = false; + + public function __construct(ReadableStreamInterface $input) + { + $this->input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return Util::pipe($this, $dest, $options); + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($data !== '') { + $this->emit('data', array( + dechex(strlen($data)) . "\r\n" . $data . "\r\n" + )); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('data', array("0\r\n\r\n")); + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/vendor/clue/buzz-react/src/Io/Sender.php b/vendor/clue/buzz-react/src/Io/Sender.php new file mode 100644 index 0000000..06c1212 --- /dev/null +++ b/vendor/clue/buzz-react/src/Io/Sender.php @@ -0,0 +1,161 @@ +<?php + +namespace Clue\React\Buzz\Io; + +use Clue\React\Buzz\Message\MessageFactory; +use Psr\Http\Message\RequestInterface; +use React\EventLoop\LoopInterface; +use React\HttpClient\Client as HttpClient; +use React\HttpClient\Response as ResponseStream; +use React\Promise\PromiseInterface; +use React\Promise\Deferred; +use React\Socket\ConnectorInterface; +use React\Stream\ReadableStreamInterface; + +/** + * [Internal] Sends requests and receives responses + * + * The `Sender` is responsible for passing the [`RequestInterface`](#requestinterface) objects to + * the underlying [`HttpClient`](https://github.com/reactphp/http-client) library + * and keeps track of its transmission and converts its reponses back to [`ResponseInterface`](#responseinterface) objects. + * + * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) + * and the default [`Connector`](https://github.com/reactphp/socket-client) and [DNS `Resolver`](https://github.com/reactphp/dns). + * + * The `Sender` class mostly exists in order to abstract changes on the underlying + * components away from this package in order to provide backwards and forwards + * compatibility. + * + * @internal You SHOULD NOT rely on this API, it is subject to change without prior notice! + * @see Browser + */ +class Sender +{ + /** + * create a new default sender attached to the given event loop + * + * This method is used internally to create the "default sender". + * + * You may also use this method if you need custom DNS or connector + * settings. You can use this method manually like this: + * + * ```php + * $connector = new \React\Socket\Connector($loop); + * $sender = \Clue\React\Buzz\Io\Sender::createFromLoop($loop, $connector); + * ``` + * + * @param LoopInterface $loop + * @param ConnectorInterface|null $connector + * @return self + */ + public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector = null, MessageFactory $messageFactory) + { + return new self(new HttpClient($loop, $connector), $messageFactory); + } + + private $http; + private $messageFactory; + + /** + * [internal] Instantiate Sender + * + * @param HttpClient $http + * @internal + */ + public function __construct(HttpClient $http, MessageFactory $messageFactory) + { + $this->http = $http; + $this->messageFactory = $messageFactory; + } + + /** + * + * @internal + * @param RequestInterface $request + * @return PromiseInterface Promise<ResponseInterface, Exception> + */ + public function send(RequestInterface $request) + { + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size !== 0) { + // automatically assign a "Content-Length" request header if the body size is known and non-empty + $request = $request->withHeader('Content-Length', (string)$size); + } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + // only assign a "Content-Length: 0" request header if the body is expected for certain methods + $request = $request->withHeader('Content-Length', '0'); + } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) { + // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown + $request = $request->withHeader('Transfer-Encoding', 'chunked'); + } else { + // do not use chunked encoding if size is known or if this is an empty request body + $size = 0; + } + + $headers = array(); + foreach ($request->getHeaders() as $name => $values) { + $headers[$name] = implode(', ', $values); + } + + $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); + + $deferred = new Deferred(function ($_, $reject) use ($requestStream) { + // close request stream if request is cancelled + $reject(new \RuntimeException('Request cancelled')); + $requestStream->close(); + }); + + $requestStream->on('error', function($error) use ($deferred) { + $deferred->reject($error); + }); + + $messageFactory = $this->messageFactory; + $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) { + // apply response header values from response stream + $deferred->resolve($messageFactory->response( + $responseStream->getVersion(), + $responseStream->getCode(), + $responseStream->getReasonPhrase(), + $responseStream->getHeaders(), + $responseStream, + $request->getMethod() + )); + }); + + if ($body instanceof ReadableStreamInterface) { + if ($body->isReadable()) { + // length unknown => apply chunked transfer-encoding + if ($size === null) { + $body = new ChunkedEncoder($body); + } + + // pipe body into request stream + // add dummy write to immediately start request even if body does not emit any data yet + $body->pipe($requestStream); + $requestStream->write(''); + + $body->on('close', $close = function () use ($deferred, $requestStream) { + $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); + $requestStream->close(); + }); + $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { + $body->removeListener('close', $close); + $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); + $requestStream->close(); + }); + $body->on('end', function () use ($close, $body) { + $body->removeListener('close', $close); + }); + } else { + // stream is not readable => end request without body + $requestStream->end(); + } + } else { + // body is fully buffered => write as one chunk + $requestStream->end((string)$body); + } + + return $deferred->promise(); + } +} diff --git a/vendor/clue/buzz-react/src/Io/Transaction.php b/vendor/clue/buzz-react/src/Io/Transaction.php new file mode 100644 index 0000000..adb5796 --- /dev/null +++ b/vendor/clue/buzz-react/src/Io/Transaction.php @@ -0,0 +1,305 @@ +<?php + +namespace Clue\React\Buzz\Io; + +use Clue\React\Buzz\Message\ResponseException; +use Clue\React\Buzz\Message\MessageFactory; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; +use React\EventLoop\LoopInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; +use React\Stream\ReadableStreamInterface; + +/** + * @internal + */ +class Transaction +{ + private $sender; + private $messageFactory; + private $loop; + + // context: http.timeout (ini_get('default_socket_timeout'): 60) + private $timeout; + + // context: http.follow_location (true) + private $followRedirects = true; + + // context: http.max_redirects (10) + private $maxRedirects = 10; + + // context: http.ignore_errors (false) + private $obeySuccessCode = true; + + private $streaming = false; + + private $maximumSize = 16777216; // 16 MiB = 2^24 bytes + + public function __construct(Sender $sender, MessageFactory $messageFactory, LoopInterface $loop) + { + $this->sender = $sender; + $this->messageFactory = $messageFactory; + $this->loop = $loop; + } + + /** + * @param array $options + * @return self returns new instance, without modifying existing instance + */ + public function withOptions(array $options) + { + $transaction = clone $this; + foreach ($options as $name => $value) { + if (property_exists($transaction, $name)) { + // restore default value if null is given + if ($value === null) { + $default = new self($this->sender, $this->messageFactory, $this->loop); + $value = $default->$name; + } + + $transaction->$name = $value; + } + } + + return $transaction; + } + + public function send(RequestInterface $request) + { + $deferred = new Deferred(function () use (&$deferred) { + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + + $deferred->numRequests = 0; + + // use timeout from options or default to PHP's default_socket_timeout (60) + $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); + + $loop = $this->loop; + $this->next($request, $deferred)->then( + function (ResponseInterface $response) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->resolve($response); + }, + function ($e) use ($deferred, $loop, &$timeout) { + if (isset($deferred->timeout)) { + $loop->cancelTimer($deferred->timeout); + unset($deferred->timeout); + } + $timeout = -1; + $deferred->reject($e); + } + ); + + if ($timeout < 0) { + return $deferred->promise(); + } + + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $that = $this; + $body->on('close', function () use ($that, $deferred, &$timeout) { + if ($timeout >= 0) { + $that->applyTimeout($deferred, $timeout); + } + }); + } else { + $this->applyTimeout($deferred, $timeout); + } + + return $deferred->promise(); + } + + /** + * @internal + * @param Deferred $deferred + * @param number $timeout + * @return void + */ + public function applyTimeout(Deferred $deferred, $timeout) + { + $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { + $deferred->reject(new \RuntimeException( + 'Request timed out after ' . $timeout . ' seconds' + )); + if (isset($deferred->pending)) { + $deferred->pending->cancel(); + unset($deferred->pending); + } + }); + } + + private function next(RequestInterface $request, Deferred $deferred) + { + $this->progress('request', array($request)); + + $that = $this; + ++$deferred->numRequests; + + $promise = $this->sender->send($request); + + if (!$this->streaming) { + $promise = $promise->then(function ($response) use ($deferred, $that) { + return $that->bufferResponse($response, $deferred); + }); + } + + $deferred->pending = $promise; + + return $promise->then( + function (ResponseInterface $response) use ($request, $that, $deferred) { + return $that->onResponse($response, $request, $deferred); + } + ); + } + + /** + * @internal + * @param ResponseInterface $response + * @return PromiseInterface Promise<ResponseInterface, Exception> + */ + public function bufferResponse(ResponseInterface $response, $deferred) + { + $stream = $response->getBody(); + + $size = $stream->getSize(); + if ($size !== null && $size > $this->maximumSize) { + $stream->close(); + return \React\Promise\reject(new \OverflowException( + 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + )); + } + + // body is not streaming => already buffered + if (!$stream instanceof ReadableStreamInterface) { + return \React\Promise\resolve($response); + } + + // buffer stream and resolve with buffered body + $messageFactory = $this->messageFactory; + $maximumSize = $this->maximumSize; + $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( + function ($body) use ($response, $messageFactory) { + return $response->withBody($messageFactory->body($body)); + }, + function ($e) use ($stream, $maximumSize) { + // try to close stream if buffering fails (or is cancelled) + $stream->close(); + + if ($e instanceof \OverflowException) { + $e = new \OverflowException( + 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 + ); + } + + throw $e; + } + ); + + $deferred->pending = $promise; + + return $promise; + } + + /** + * @internal + * @param ResponseInterface $response + * @param RequestInterface $request + * @throws ResponseException + * @return ResponseInterface|PromiseInterface + */ + public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) + { + $this->progress('response', array($response, $request)); + + // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled + // @link https://tools.ietf.org/html/rfc7231#section-6.4 + if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { + return $this->onResponseRedirect($response, $request, $deferred); + } + + // only status codes 200-399 are considered to be valid, reject otherwise + if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { + throw new ResponseException($response); + } + + // resolve our initial promise + return $response; + } + + /** + * @param ResponseInterface $response + * @param RequestInterface $request + * @return PromiseInterface + * @throws \RuntimeException + */ + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) + { + // resolve location relative to last request URI + $location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location')); + + $request = $this->makeRedirectRequest($request, $location); + $this->progress('redirect', array($request)); + + if ($deferred->numRequests >= $this->maxRedirects) { + throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); + } + + return $this->next($request, $deferred); + } + + /** + * @param RequestInterface $request + * @param UriInterface $location + * @return RequestInterface + */ + private function makeRedirectRequest(RequestInterface $request, UriInterface $location) + { + $originalHost = $request->getUri()->getHost(); + $request = $request + ->withoutHeader('Host') + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + + // Remove authorization if changing hostnames (but not if just changing ports or protocols). + if ($location->getHost() !== $originalHost) { + $request = $request->withoutHeader('Authorization'); + } + + // naïve approach.. + $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; + + return $this->messageFactory->request($method, $location, $request->getHeaders()); + } + + private function progress($name, array $args = array()) + { + return; + + echo $name; + + foreach ($args as $arg) { + echo ' '; + if ($arg instanceof ResponseInterface) { + echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); + } elseif ($arg instanceof RequestInterface) { + echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); + } else { + echo $arg; + } + } + + echo PHP_EOL; + } +} diff --git a/vendor/clue/buzz-react/src/Message/MessageFactory.php b/vendor/clue/buzz-react/src/Message/MessageFactory.php new file mode 100644 index 0000000..8a3dd6d --- /dev/null +++ b/vendor/clue/buzz-react/src/Message/MessageFactory.php @@ -0,0 +1,139 @@ +<?php + +namespace Clue\React\Buzz\Message; + +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; +use RingCentral\Psr7\Request; +use RingCentral\Psr7\Response; +use RingCentral\Psr7\Uri; +use React\Stream\ReadableStreamInterface; + +/** + * @internal + */ +class MessageFactory +{ + /** + * Creates a new instance of RequestInterface for the given request parameters + * + * @param string $method + * @param string|UriInterface $uri + * @param array $headers + * @param string|ReadableStreamInterface $content + * @param string $protocolVersion + * @return Request + */ + public function request($method, $uri, $headers = array(), $content = '', $protocolVersion = '1.1') + { + return new Request($method, $uri, $headers, $this->body($content), $protocolVersion); + } + + /** + * Creates a new instance of ResponseInterface for the given response parameters + * + * @param string $protocolVersion + * @param int $status + * @param string $reason + * @param array $headers + * @param ReadableStreamInterface|string $body + * @param ?string $requestMethod + * @return Response + * @uses self::body() + */ + public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null) + { + $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); + + if ($body instanceof ReadableStreamInterface) { + $length = null; + $code = $response->getStatusCode(); + if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $length = null; + } elseif ($response->hasHeader('Content-Length')) { + $length = (int)$response->getHeaderLine('Content-Length'); + } + + $response = $response->withBody(new ReadableBodyStream($body, $length)); + } + + return $response; + } + + /** + * Creates a new instance of StreamInterface for the given body contents + * + * @param ReadableStreamInterface|string $body + * @return StreamInterface + */ + public function body($body) + { + if ($body instanceof ReadableStreamInterface) { + return new ReadableBodyStream($body); + } + + return \RingCentral\Psr7\stream_for($body); + } + + /** + * Creates a new instance of UriInterface for the given URI string + * + * @param string $uri + * @return UriInterface + */ + public function uri($uri) + { + return new Uri($uri); + } + + /** + * Creates a new instance of UriInterface for the given URI string relative to the given base URI + * + * @param UriInterface $base + * @param string $uri + * @return UriInterface + */ + public function uriRelative(UriInterface $base, $uri) + { + return Uri::resolve($base, $uri); + } + + /** + * Resolves the given relative or absolute $uri by appending it behind $this base URI + * + * The given $uri parameter can be either a relative or absolute URI and + * as such can not contain any URI template placeholders. + * + * As such, the outcome of this method represents a valid, absolute URI + * which will be returned as an instance implementing `UriInterface`. + * + * If the given $uri is a relative URI, it will simply be appended behind $base URI. + * + * If the given $uri is an absolute URI, it will simply be returned as-is. + * + * @param UriInterface $uri + * @param UriInterface $base + * @return UriInterface + */ + public function expandBase(UriInterface $uri, UriInterface $base) + { + if ($uri->getScheme() !== '') { + return $uri; + } + + $uri = (string)$uri; + $base = (string)$base; + + if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') { + $base .= '/'; + } + + if (isset($uri[0]) && $uri[0] === '/') { + $uri = substr($uri, 1); + } + + return $this->uri($base . $uri); + } +} diff --git a/vendor/clue/buzz-react/src/Message/ReadableBodyStream.php b/vendor/clue/buzz-react/src/Message/ReadableBodyStream.php new file mode 100644 index 0000000..eac27f7 --- /dev/null +++ b/vendor/clue/buzz-react/src/Message/ReadableBodyStream.php @@ -0,0 +1,153 @@ +<?php + +namespace Clue\React\Buzz\Message; + +use Evenement\EventEmitter; +use Psr\Http\Message\StreamInterface; +use React\Stream\ReadableStreamInterface; +use React\Stream\Util; +use React\Stream\WritableStreamInterface; + +/** + * @internal + */ +class ReadableBodyStream extends EventEmitter implements ReadableStreamInterface, StreamInterface +{ + private $input; + private $position = 0; + private $size; + private $closed = false; + + public function __construct(ReadableStreamInterface $input, $size = null) + { + $this->input = $input; + $this->size = $size; + + $that = $this; + $pos =& $this->position; + $input->on('data', function ($data) use ($that, &$pos, $size) { + $that->emit('data', array($data)); + + $pos += \strlen($data); + if ($size !== null && $pos >= $size) { + $that->handleEnd(); + } + }); + $input->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + $that->close(); + }); + $input->on('end', array($that, 'handleEnd')); + $input->on('close', array($that, 'close')); + } + + public function close() + { + if (!$this->closed) { + $this->closed = true; + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function eof() + { + return !$this->isReadable(); + } + + public function __toString() + { + return ''; + } + + public function detach() + { + throw new \BadMethodCallException(); + } + + public function getSize() + { + return $this->size; + } + + public function tell() + { + throw new \BadMethodCallException(); + } + + public function isSeekable() + { + return false; + } + + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + public function rewind() + { + throw new \BadMethodCallException(); + } + + public function isWritable() + { + return false; + } + + public function write($string) + { + throw new \BadMethodCallException(); + } + + public function read($length) + { + throw new \BadMethodCallException(); + } + + public function getContents() + { + throw new \BadMethodCallException(); + } + + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } + + /** @internal */ + public function handleEnd() + { + if ($this->position !== $this->size && $this->size !== null) { + $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); + } else { + $this->emit('end'); + } + + $this->close(); + } +} diff --git a/vendor/clue/buzz-react/src/Message/ResponseException.php b/vendor/clue/buzz-react/src/Message/ResponseException.php new file mode 100644 index 0000000..081103a --- /dev/null +++ b/vendor/clue/buzz-react/src/Message/ResponseException.php @@ -0,0 +1,43 @@ +<?php + +namespace Clue\React\Buzz\Message; + +use RuntimeException; +use Psr\Http\Message\ResponseInterface; + +/** + * The `ResponseException` is an `Exception` sub-class that will be used to reject + * a request promise if the remote server returns a non-success status code + * (anything but 2xx or 3xx). + * You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + * + * The `getCode(): int` method can be used to + * return the HTTP response status code. + */ +class ResponseException extends RuntimeException +{ + private $response; + + public function __construct(ResponseInterface $response, $message = null, $code = null, $previous = null) + { + if ($message === null) { + $message = 'HTTP status code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ')'; + } + if ($code === null) { + $code = $response->getStatusCode(); + } + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + /** + * Access its underlying [`ResponseInterface`](#responseinterface) object. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} 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/composer.json b/vendor/clue/connection-manager-extra/composer.json new file mode 100644 index 0000000..7534f80 --- /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/socket": "^1.0 || ^0.8 || ^0.7", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", + "react/promise": "^2.1 || ^1.2.1", + "react/promise-timer": "^1.1" + }, + "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..b5112c6 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerDelay.php @@ -0,0 +1,30 @@ +<?php + +namespace ConnectionManager\Extra; + +use React\Socket\ConnectorInterface; +use React\EventLoop\LoopInterface; +use React\Promise\Timer; + +class ConnectionManagerDelay implements ConnectorInterface +{ + private $connectionManager; + private $delay; + private $loop; + + public function __construct(ConnectorInterface $connectionManager, $delay, LoopInterface $loop) + { + $this->connectionManager = $connectionManager; + $this->delay = $delay; + $this->loop = $loop; + } + + 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..10f3f5a --- /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\CancellablePromiseInterface; + +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 CancellablePromiseInterface) { + $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..5ec0872 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/ConnectionManagerTimeout.php @@ -0,0 +1,35 @@ +<?php + +namespace ConnectionManager\Extra; + +use React\Socket\ConnectorInterface; +use React\EventLoop\LoopInterface; +use React\Promise\Timer; + +class ConnectionManagerTimeout implements ConnectorInterface +{ + private $connectionManager; + private $timeout; + private $loop; + + public function __construct(ConnectorInterface $connectionManager, $timeout, LoopInterface $loop) + { + $this->connectionManager = $connectionManager; + $this->timeout = $timeout; + $this->loop = $loop; + } + + 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..c1eb9cf --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConcurrent.php @@ -0,0 +1,36 @@ +<?php + +namespace ConnectionManager\Extra\Multiple; + +use ConnectionManager\Extra\Multiple\ConnectionManagerConsecutive; +use React\Promise; +use React\Promise\CancellablePromiseInterface; + +class ConnectionManagerConcurrent extends ConnectionManagerConsecutive +{ + public function connect($uri) + { + $all = array(); + foreach ($this->managers as $connector) { + /* @var $connection 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 CancellablePromiseInterface) { + $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..1474b85 --- /dev/null +++ b/vendor/clue/connection-manager-extra/src/Multiple/ConnectionManagerConsecutive.php @@ -0,0 +1,62 @@ +<?php + +namespace ConnectionManager\Extra\Multiple; + +use React\Socket\ConnectorInterface; +use React\Promise; +use UnderflowException; +use React\Promise\CancellablePromiseInterface; + +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 CancellablePromiseInterface) { + $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/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/composer.json b/vendor/clue/http-proxy-react/composer.json new file mode 100644 index 0000000..9840cf0 --- /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": " ^2.1 || ^1.2.1", + "react/socket": "^1.9", + "ringcentral/psr7": "^1.2" + }, + "require-dev": { + "clue/block-react": "^1.1", + "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..0bca17e --- /dev/null +++ b/vendor/clue/http-proxy-react/src/ProxyConnector.php @@ -0,0 +1,274 @@ +<?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($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 &$one) { + if (isset($one['args'])) { + foreach ($one['args'] as &$arg) { + if ($arg instanceof \Closure) { + $arg = 'Object(' . get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($e, $trace); + }); + + return $deferred->promise(); + } +} 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/composer.json b/vendor/clue/mq-react/composer.json new file mode 100644 index 0000000..d6e7793 --- /dev/null +++ b/vendor/clue/mq-react/composer.json @@ -0,0 +1,29 @@ +{ + "name": "clue/mq-react", + "description": "Mini Queue, the lightweight in-memory message queue to concurrently do many (but not too many) things at once, built on top of ReactPHP", + "keywords": ["Message Queue", "Mini Queue", "job", "message", "worker", "queue", "rate limit", "throttle", "concurrency", "ReactPHP", "async"], + "homepage": "https://github.com/clue/reactphp-mq", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "autoload": { + "psr-4": { "Clue\\React\\Mq\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Mq\\": "tests/" } + }, + "require": { + "php": ">=5.3", + "react/promise": "^2.2.1 || ^1.2.1" + }, + "require-dev": { + "clue/block-react": "^1.0", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2", + "react/http": "^1.5" + } +} diff --git a/vendor/clue/mq-react/src/Queue.php b/vendor/clue/mq-react/src/Queue.php new file mode 100644 index 0000000..7d399ac --- /dev/null +++ b/vendor/clue/mq-react/src/Queue.php @@ -0,0 +1,448 @@ +<?php + +namespace Clue\React\Mq; + +use React\Promise; +use React\Promise\CancellablePromiseInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; + +/** + * The `Queue` is responsible for managing your operations and ensuring not too + * many operations are executed at once. It's a very simple and lightweight + * in-memory implementation of the + * [leaky bucket](https://en.wikipedia.org/wiki/Leaky_bucket#As_a_queue) algorithm. + * + * This means that you control how many operations can be executed concurrently. + * If you add a job to the queue and it still below the limit, it will be executed + * immediately. If you keep adding new jobs to the queue and its concurrency limit + * is reached, it will not start a new operation and instead queue this for future + * execution. Once one of the pending operations complete, it will pick the next + * job from the queue and execute this operation. + */ +class Queue implements \Countable +{ + private $concurrency; + private $limit; + private $handler; + + private $pending = 0; + private $queue = array(); + + /** + * Concurrently process all given jobs through the given `$handler`. + * + * This is a convenience method which uses the `Queue` internally to + * schedule all jobs while limiting concurrency to ensure no more than + * `$concurrency` jobs ever run at once. It will return a promise which + * resolves with the results of all jobs on success. + * + * ```php + * $browser = new React\Http\Browser(); + * + * $promise = Queue::all(3, $urls, function ($url) use ($browser) { + * return $browser->get($url); + * }); + * + * $promise->then(function (array $responses) { + * echo 'All ' . count($responses) . ' successful!' . PHP_EOL; + * }); + * ``` + * + * If either of the jobs fail, it will reject the resulting promise and will + * try to cancel all outstanding jobs. Similarly, calling `cancel()` on the + * resulting promise will try to cancel all outstanding jobs. See + * [promises](#promises) and [cancellation](#cancellation) for details. + * + * The `$concurrency` parameter sets a new soft limit for the maximum number + * of jobs to handle concurrently. Finding a good concurrency limit depends + * on your particular use case. It's common to limit concurrency to a rather + * small value, as doing more than a dozen of things at once may easily + * overwhelm the receiving side. Using a `1` value will ensure that all jobs + * are processed one after another, effectively creating a "waterfall" of + * jobs. Using a value less than 1 will reject with an + * `InvalidArgumentException` without processing any jobs. + * + * ```php + * // handle up to 10 jobs concurrently + * $promise = Queue::all(10, $jobs, $handler); + * ``` + * + * ```php + * // handle each job after another without concurrency (waterfall) + * $promise = Queue::all(1, $jobs, $handler); + * ``` + * + * The `$jobs` parameter must be an array with all jobs to process. Each + * value in this array will be passed to the `$handler` to start one job. + * The array keys will be preserved in the resulting array, while the array + * values will be replaced with the job results as returned by the + * `$handler`. If this array is empty, this method will resolve with an + * empty array without processing any jobs. + * + * The `$handler` parameter must be a valid callable that accepts your job + * parameters, invokes the appropriate operation and returns a Promise as a + * placeholder for its future result. If the given argument is not a valid + * callable, this method will reject with an `InvalidArgumentException` + * without processing any jobs. + * + * ```php + * // using a Closure as handler is usually recommended + * $promise = Queue::all(10, $jobs, function ($url) use ($browser) { + * return $browser->get($url); + * }); + * ``` + * + * ```php + * // accepts any callable, so PHP's array notation is also supported + * $promise = Queue::all(10, $jobs, array($browser, 'get')); + * ``` + * + * > Keep in mind that returning an array of response messages means that + * the whole response body has to be kept in memory. + * + * @param int $concurrency concurrency soft limit + * @param array $jobs + * @param callable $handler + * @return PromiseInterface Returns a Promise<mixed[]> which resolves with an array of all resolution values + * or rejects when any of the operations reject. + */ + public static function all($concurrency, array $jobs, $handler) + { + try { + // limit number of concurrent operations + $q = new self($concurrency, null, $handler); + } catch (\InvalidArgumentException $e) { + // reject if $concurrency or $handler is invalid + return Promise\reject($e); + } + + // try invoking all operations and automatically queue excessive ones + $promises = array_map($q, $jobs); + + return new Promise\Promise(function ($resolve, $reject) use ($promises) { + Promise\all($promises)->then($resolve, function ($e) use ($promises, $reject) { + // cancel all pending promises if a single promise fails + foreach (array_reverse($promises) as $promise) { + if ($promise instanceof CancellablePromiseInterface) { + $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 CancellablePromiseInterface) { + $promise->cancel(); + } + } + }); + } + + /** + * Concurrently process the given jobs through the given `$handler` and + * resolve with first resolution value. + * + * This is a convenience method which uses the `Queue` internally to + * schedule all jobs while limiting concurrency to ensure no more than + * `$concurrency` jobs ever run at once. It will return a promise which + * resolves with the result of the first job on success and will then try + * to `cancel()` all outstanding jobs. + * + * ```php + * $browser = new React\Http\Browser(); + * + * $promise = Queue::any(3, $urls, function ($url) use ($browser) { + * return $browser->get($url); + * }); + * + * $promise->then(function (ResponseInterface $response) { + * echo 'First response: ' . $response->getBody() . PHP_EOL; + * }); + * ``` + * + * If all of the jobs fail, it will reject the resulting promise. Similarly, + * calling `cancel()` on the resulting promise will try to cancel all + * outstanding jobs. See [promises](#promises) and + * [cancellation](#cancellation) for details. + * + * The `$concurrency` parameter sets a new soft limit for the maximum number + * of jobs to handle concurrently. Finding a good concurrency limit depends + * on your particular use case. It's common to limit concurrency to a rather + * small value, as doing more than a dozen of things at once may easily + * overwhelm the receiving side. Using a `1` value will ensure that all jobs + * are processed one after another, effectively creating a "waterfall" of + * jobs. Using a value less than 1 will reject with an + * `InvalidArgumentException` without processing any jobs. + * + * ```php + * // handle up to 10 jobs concurrently + * $promise = Queue::any(10, $jobs, $handler); + * ``` + * + * ```php + * // handle each job after another without concurrency (waterfall) + * $promise = Queue::any(1, $jobs, $handler); + * ``` + * + * The `$jobs` parameter must be an array with all jobs to process. Each + * value in this array will be passed to the `$handler` to start one job. + * The array keys have no effect, the promise will simply resolve with the + * job results of the first successful job as returned by the `$handler`. + * If this array is empty, this method will reject without processing any + * jobs. + * + * The `$handler` parameter must be a valid callable that accepts your job + * parameters, invokes the appropriate operation and returns a Promise as a + * placeholder for its future result. If the given argument is not a valid + * callable, this method will reject with an `InvalidArgumentExceptionn` + * without processing any jobs. + * + * ```php + * // using a Closure as handler is usually recommended + * $promise = Queue::any(10, $jobs, function ($url) use ($browser) { + * return $browser->get($url); + * }); + * ``` + * + * ```php + * // accepts any callable, so PHP's array notation is also supported + * $promise = Queue::any(10, $jobs, array($browser, 'get')); + * ``` + * + * @param int $concurrency concurrency soft limit + * @param array $jobs + * @param callable $handler + * @return PromiseInterface Returns a Promise<mixed> which resolves with a single resolution value + * or rejects when all of the operations reject. + */ + public static function any($concurrency, array $jobs, $handler) + { + // explicitly reject with empty jobs (https://github.com/reactphp/promise/pull/34) + if (!$jobs) { + return Promise\reject(new \UnderflowException('No jobs given')); + } + + try { + // limit number of concurrent operations + $q = new self($concurrency, null, $handler); + } catch (\InvalidArgumentException $e) { + // reject if $concurrency or $handler is invalid + return Promise\reject($e); + } + + // try invoking all operations and automatically queue excessive ones + $promises = array_map($q, $jobs); + + return new Promise\Promise(function ($resolve, $reject) use ($promises) { + Promise\any($promises)->then(function ($result) use ($promises, $resolve) { + // cancel all pending promises if a single result is ready + foreach (array_reverse($promises) as $promise) { + if ($promise instanceof CancellablePromiseInterface) { + $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 CancellablePromiseInterface) { + $promise->cancel(); + } + } + }); + } + + /** + * Instantiates a new queue object. + * + * You can create any number of queues, for example when you want to apply + * different limits to different kind of operations. + * + * The `$concurrency` parameter sets a new soft limit for the maximum number + * of jobs to handle concurrently. Finding a good concurrency limit depends + * on your particular use case. It's common to limit concurrency to a rather + * small value, as doing more than a dozen of things at once may easily + * overwhelm the receiving side. + * + * The `$limit` parameter sets a new hard limit on how many jobs may be + * outstanding (kept in memory) at once. Depending on your particular use + * case, it's usually safe to keep a few hundreds or thousands of jobs in + * memory. If you do not want to apply an upper limit, you can pass a `null` + * value which is semantically more meaningful than passing a big number. + * + * ```php + * // handle up to 10 jobs concurrently, but keep no more than 1000 in memory + * $q = new Queue(10, 1000, $handler); + * ``` + * + * ```php + * // handle up to 10 jobs concurrently, do not limit queue size + * $q = new Queue(10, null, $handler); + * ``` + * + * ```php + * // handle up to 10 jobs concurrently, reject all further jobs + * $q = new Queue(10, 10, $handler); + * ``` + * + * The `$handler` parameter must be a valid callable that accepts your job + * parameters, invokes the appropriate operation and returns a Promise as a + * placeholder for its future result. + * + * ```php + * // using a Closure as handler is usually recommended + * $q = new Queue(10, null, function ($url) use ($browser) { + * return $browser->get($url); + * }); + * ``` + * + * ```php + * // PHP's array callable as handler is also supported + * $q = new Queue(10, null, array($browser, 'get')); + * ``` + * + * @param int $concurrency concurrency soft limit + * @param int|null $limit queue hard limit or NULL=unlimited + * @param callable $handler + * @throws \InvalidArgumentException + */ + public function __construct($concurrency, $limit, $handler) + { + if ($concurrency < 1 || ($limit !== null && ($limit < 1 || $concurrency > $limit))) { + throw new \InvalidArgumentException('Invalid limit given'); + } + if (!is_callable($handler)) { + throw new \InvalidArgumentException('Invalid handler given'); + } + + $this->concurrency = $concurrency; + $this->limit = $limit; + $this->handler = $handler; + } + + /** + * The Queue instance is invokable, so that invoking `$q(...$args)` will + * actually be forwarded as `$handler(...$args)` as given in the + * `$handler` argument when concurrency is still below limits. + * + * Each operation may take some time to complete, but due to its async nature you + * can actually start any number of (queued) operations. Once the concurrency limit + * is reached, this invocation will simply be queued and this will return a pending + * promise which will start the actual operation once another operation is + * completed. This means that this is handled entirely transparently and you do not + * need to worry about this concurrency limit yourself. + * + * @return \React\Promise\PromiseInterface + */ + public function __invoke() + { + // happy path: simply invoke handler if we're below concurrency limit + if ($this->pending < $this->concurrency) { + ++$this->pending; + + // invoke handler and await its resolution before invoking next queued job + return $this->await( + call_user_func_array($this->handler, func_get_args()) + ); + } + + // we're currently above concurrency limit, make sure we do not exceed maximum queue limit + if ($this->limit !== null && $this->count() >= $this->limit) { + return Promise\reject(new \OverflowException('Maximum queue limit of ' . $this->limit . ' exceeded')); + } + + // if we reach this point, then this job will need to be queued + // get next queue position + $queue =& $this->queue; + $queue[] = null; + end($queue); + $id = key($queue); + + $deferred = new Deferred(function ($_, $reject) use (&$queue, $id, &$deferred) { + // forward cancellation to pending operation if it is currently executing + if (isset($deferred->pending) && $deferred->pending instanceof CancellablePromiseInterface) { + $deferred->pending->cancel(); + } + unset($deferred->pending); + + if (isset($deferred->args)) { + // queued promise cancelled before its handler is invoked + // remove from queue and reject explicitly + unset($queue[$id], $deferred->args); + $reject(new \RuntimeException('Cancelled queued job before processing started')); + } + }); + + // queue job to process if number of pending jobs is below concurrency limit again + $deferred->args = func_get_args(); + $queue[$id] = $deferred; + + return $deferred->promise(); + } + + #[\ReturnTypeWillChange] + public function count() + { + return $this->pending + count($this->queue); + } + + /** + * @internal + */ + public function await(PromiseInterface $promise) + { + $that = $this; + + return $promise->then(function ($result) use ($that) { + $that->processQueue(); + + return $result; + }, function ($error) use ($that) { + $that->processQueue(); + + return Promise\reject($error); + }); + } + + /** + * @internal + */ + public function processQueue() + { + // skip if we're still above concurrency limit or there's no queued job waiting + if (--$this->pending >= $this->concurrency || !$this->queue) { + return; + } + + /* @var $deferred Deferred */ + $deferred = reset($this->queue); + unset($this->queue[key($this->queue)]); + + // once number of pending jobs is below concurrency limit again: + // await this situation, invoke handler and await its resolution before invoking next queued job + ++$this->pending; + + $promise = call_user_func_array($this->handler, $deferred->args); + $deferred->pending = $promise; + unset($deferred->args); + + // invoke handler and await its resolution before invoking next queued job + $this->await($promise)->then( + function ($result) use ($deferred) { + unset($deferred->pending); + $deferred->resolve($result); + }, + function ($e) use ($deferred) { + unset($deferred->pending); + $deferred->reject($e); + } + ); + } +} 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/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-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/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/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/composer.json b/vendor/clue/soap-react/composer.json new file mode 100644 index 0000000..3b7fc4a --- /dev/null +++ b/vendor/clue/soap-react/composer.json @@ -0,0 +1,27 @@ +{ + "name": "clue/soap-react", + "description": "Simple, async SOAP webservice client library, built on top of ReactPHP", + "keywords": ["SOAP", "SoapClient", "WebService", "WSDL", "ReactPHP"], + "homepage": "https://github.com/clue/reactphp-soap", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "autoload": { + "psr-4": { "Clue\\React\\Soap\\": "src/" } + }, + "require": { + "php": ">=5.3", + "clue/buzz-react": "^2.5", + "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3", + "react/promise": "^2.1 || ^1.2", + "ext-soap": "*" + }, + "require-dev": { + "clue/block-react": "^1.0", + "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" + } +} diff --git a/vendor/clue/soap-react/src/Client.php b/vendor/clue/soap-react/src/Client.php new file mode 100644 index 0000000..85ee7af --- /dev/null +++ b/vendor/clue/soap-react/src/Client.php @@ -0,0 +1,323 @@ +<?php + +namespace Clue\React\Soap; + +use Clue\React\Buzz\Browser; +use Clue\React\Soap\Protocol\ClientDecoder; +use Clue\React\Soap\Protocol\ClientEncoder; +use Psr\Http\Message\ResponseInterface; +use React\Promise\Deferred; +use React\Promise\PromiseInterface; + +/** + * The `Client` class is responsible for communication with the remote SOAP + * WebService server. + * + * It requires a [`Browser`](https://github.com/clue/reactphp-buzz#browser) object + * bound to the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) + * in order to handle async requests, the WSDL file contents and an optional + * array of SOAP options: + * + * ```php + * $loop = React\EventLoop\Factory::create(); + * $browser = new Clue\React\Buzz\Browser($loop); + * + * $wsdl = '<?xml …'; + * $options = array(); + * + * $client = new Client($browser, $wsdl, $options); + * ``` + * + * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, + * proxy servers etc.), you can explicitly pass a custom instance of the + * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + * to the [`Browser`](https://github.com/clue/reactphp-buzz#browser) instance: + * + * ```php + * $connector = new \React\Socket\Connector($loop, array( + * 'dns' => '127.0.0.1', + * 'tcp' => array( + * 'bindto' => '192.168.10.1:0' + * ), + * 'tls' => array( + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ) + * )); + * + * $browser = new Browser($loop, $connector); + * $client = new Client($browser, $wsdl); + * ``` + * + * The `Client` works similar to PHP's `SoapClient` (which it uses under the + * hood), but leaves you the responsibility to load the WSDL file. This allows + * you to use local WSDL files, WSDL files from a cache or the most common form, + * downloading the WSDL file contents from an URL through the `Browser`: + * + * ```php + * $browser = new Browser($loop); + * + * $browser->get($url)->then( + * function (ResponseInterface $response) use ($browser) { + * // WSDL file is ready, create client + * $client = new Client($browser, (string)$response->getBody()); + * + * // do something… + * }, + * function (Exception $e) { + * // an error occured while trying to download the WSDL + * } + * ); + * ``` + * + * The `Client` constructor loads the given WSDL file contents into memory and + * parses its definition. If the given WSDL file is invalid and can not be + * parsed, this will throw a `SoapFault`: + * + * ```php + * try { + * $client = new Client($browser, $wsdl); + * } catch (SoapFault $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * } + * ``` + * + * > Note that if you have `ext-xdebug` loaded, this may halt with a fatal + * error instead of throwing a `SoapFault`. It is not recommended to use this + * extension in production, so this should only ever affect test environments. + * + * The `Client` constructor accepts an array of options. All given options will + * be passed through to the underlying `SoapClient`. However, not all options + * make sense in this async implementation and as such may not have the desired + * effect. See also [`SoapClient`](http://php.net/manual/en/soapclient.soapclient.php) + * documentation for more details. + * + * If working in WSDL mode, the `$options` parameter is optional. If working in + * non-WSDL mode, the WSDL parameter must be set to `null` and the options + * parameter must contain the `location` and `uri` options, where `location` is + * the URL of the SOAP server to send the request to, and `uri` is the target + * namespace of the SOAP service: + * + * ```php + * $client = new Client($browser, null, array( + * 'location' => 'http://example.com', + * 'uri' => 'http://ping.example.com', + * )); + * ``` + * + * Similarly, if working in WSDL mode, the `location` option can be used to + * explicitly overwrite the URL of the SOAP server to send the request to: + * + * ```php + * $client = new Client($browser, $wsdl, array( + * 'location' => 'http://example.com' + * )); + * ``` + * + * You can use the `soap_version` option to change from the default SOAP 1.1 to + * use SOAP 1.2 instead: + * + * ```php + * $client = new Client($browser, $wsdl, array( + * 'soap_version' => SOAP_1_2 + * )); + * ``` + * + * You can use the `classmap` option to map certain WSDL types to PHP classes + * like this: + * + * ```php + * $client = new Client($browser, $wsdl, array( + * 'classmap' => array( + * 'getBankResponseType' => BankResponse::class + * ) + * )); + * ``` + * + * The `proxy_host` option (and family) is not supported by this library. As an + * alternative, you can configure the given `$browser` instance to use an + * [HTTP proxy server](https://github.com/clue/reactphp-buzz#http-proxy). + * If you find any other option is missing or not supported here, PRs are much + * appreciated! + * + * All public methods of the `Client` are considered *advanced usage*. + * If you want to call RPC functions, see below for the [`Proxy`](#proxy) class. + */ +class Client +{ + private $browser; + private $encoder; + private $decoder; + + /** + * Instantiate a new SOAP client for the given WSDL contents. + * + * @param Browser $browser + * @param string|null $wsdlContents + * @param array $options + */ + public function __construct(Browser $browser, $wsdlContents, array $options = array()) + { + $wsdl = $wsdlContents !== null ? 'data://text/plain;base64,' . base64_encode($wsdlContents) : null; + + // Accept HTTP responses with error status codes as valid responses. + // This is done in order to process these error responses through the normal SOAP decoder. + // Additionally, we explicitly limit number of redirects to zero because following redirects makes little sense + // because it transforms the POST request to a GET one and hence loses the SOAP request body. + $browser = $browser->withOptions(array( + 'obeySuccessCode' => false, + 'followRedirects' => true, + 'maxRedirects' => 0 + )); + + $this->browser = $browser; + $this->encoder = new ClientEncoder($wsdl, $options); + $this->decoder = new ClientDecoder($wsdl, $options); + } + + /** + * Queue the given function to be sent via SOAP and wait for a response from the remote web service. + * + * ```php + * // advanced usage, see Proxy for recommended alternative + * $promise = $client->soapCall('ping', array('hello', 42)); + * ``` + * + * Note: This is considered *advanced usage*, you may want to look into using the [`Proxy`](#proxy) instead. + * + * ```php + * $proxy = new Proxy($client); + * $promise = $proxy->ping('hello', 42); + * ``` + * + * @param string $name + * @param mixed[] $args + * @return PromiseInterface Returns a Promise<mixed, Exception> + */ + public function soapCall($name, $args) + { + try { + $request = $this->encoder->encode($name, $args); + } catch (\Exception $e) { + $deferred = new Deferred(); + $deferred->reject($e); + return $deferred->promise(); + } + + $decoder = $this->decoder; + + return $this->browser->send($request)->then( + function (ResponseInterface $response) use ($decoder, $name) { + // HTTP response received => decode results for this function call + return $decoder->decode($name, (string)$response->getBody()); + } + ); + } + + /** + * Returns an array of functions defined in the WSDL. + * + * It returns the equivalent of PHP's + * [`SoapClient::__getFunctions()`](http://php.net/manual/en/soapclient.getfunctions.php). + * In non-WSDL mode, this method returns `null`. + * + * @return string[]|null + */ + public function getFunctions() + { + return $this->encoder->__getFunctions(); + } + + /** + * Returns an array of types defined in the WSDL. + * + * It returns the equivalent of PHP's + * [`SoapClient::__getTypes()`](http://php.net/manual/en/soapclient.gettypes.php). + * In non-WSDL mode, this method returns `null`. + * + * @return string[]|null + */ + public function getTypes() + { + return $this->encoder->__getTypes(); + } + + /** + * Returns the location (URI) of the given webservice `$function`. + * + * Note that this is not to be confused with the WSDL file location. + * A WSDL file can contain any number of function definitions. + * It's very common that all of these functions use the same location definition. + * However, technically each function can potentially use a different location. + * + * The `$function` parameter should be a string with the the SOAP function name. + * See also [`getFunctions()`](#getfunctions) for a list of all available functions. + * + * ```php + * assert('http://example.com/soap/service' === $client->getLocation('echo')); + * ``` + * + * For easier access, this function also accepts a numeric function index. + * It then uses [`getFunctions()`](#getfunctions) internally to get the function + * name for the given index. + * This is particularly useful for the very common case where all functions use the + * same location and accessing the first location is sufficient. + * + * ```php + * assert('http://example.com/soap/service' === $client->getLocation(0)); + * ``` + * + * When the `location` option has been set in the `Client` constructor + * (such as when in non-WSDL mode) or via the `withLocation()` method, this + * method returns the value of the given location. + * + * Passing a `$function` not defined in the WSDL file will throw a `SoapFault`. + * + * @param string|int $function + * @return string + * @throws \SoapFault if given function does not exist + * @see self::getFunctions() + */ + public function getLocation($function) + { + if (is_int($function)) { + $functions = $this->getFunctions(); + if (isset($functions[$function]) && preg_match('/^\w+ (\w+)\(/', $functions[$function], $match)) { + $function = $match[1]; + } + } + + // encode request for given $function + return (string)$this->encoder->encode($function, array())->getUri(); + } + + /** + * Returns a new `Client` with the updated location (URI) for all functions. + * + * Note that this is not to be confused with the WSDL file location. + * A WSDL file can contain any number of function definitions. + * It's very common that all of these functions use the same location definition. + * However, technically each function can potentially use a different location. + * + * ```php + * $client = $client->withLocation('http://example.com/soap'); + * + * assert('http://example.com/soap' === $client->getLocation('echo')); + * ``` + * + * As an alternative to this method, you can also set the `location` option + * in the `Client` constructor (such as when in non-WSDL mode). + * + * @param string $location + * @return self + * @see self::getLocation() + */ + public function withLocation($location) + { + $client = clone $this; + $client->encoder = clone $this->encoder; + $client->encoder->__setLocation($location); + + return $client; + } +} 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..39cb745 --- /dev/null +++ b/vendor/clue/soap-react/src/Protocol/ClientDecoder.php @@ -0,0 +1,53 @@ +<?php + +namespace Clue\React\Soap\Protocol; + +use \SoapClient; + +/** + * @internal + */ +final class ClientDecoder extends SoapClient +{ + private $response = null; + + /** + * Decodes the SOAP response / return value from the given SOAP envelope (HTTP response body) + * + * @param string $function + * @param string $response + * @return mixed + * @throws \SoapFault if response indicates a fault (error condition) or is invalid + */ + public function decode($function, $response) + { + // Temporarily save response internally for further processing + $this->response = $response; + + // Let's pretend we just invoked the given SOAP function. + // This won't actually invoke anything (see `__doRequest()`), but this + // requires a valid function name to match its definition in the WSDL. + // Internally, simply use the injected response to parse its results. + $ret = $this->__soapCall($function, array()); + $this->response = null; + + return $ret; + } + + /** + * Overwrites the internal request logic to parse the response + * + * By overwriting this method, we can skip the actual request sending logic + * and still use the internal parsing logic by injecting the response as + * the return code in this method. This will implicitly be invoked by the + * call to `pseudoCall()` in the above `decode()` method. + * + * @see SoapClient::__doRequest() + */ + public function __doRequest($request, $location, $action, $version, $one_way = 0) + { + // the actual result doesn't actually matter, just return the given result + // this will be processed internally and will return the parsed result + return $this->response; + } +} 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..e9d0018 --- /dev/null +++ b/vendor/clue/soap-react/src/Protocol/ClientEncoder.php @@ -0,0 +1,69 @@ +<?php + +namespace Clue\React\Soap\Protocol; + +use \SoapClient; +use RingCentral\Psr7\Request; + +/** + * @internal + */ +final class ClientEncoder extends SoapClient +{ + private $request = null; + + /** + * Encodes the given RPC function name and arguments as a SOAP request + * + * @param string $name + * @param array $args + * @return Request + * @throws \SoapFault if request is invalid according to WSDL + */ + public function encode($name, $args) + { + $this->__soapCall($name, $args); + + $request = $this->request; + $this->request = null; + + return $request; + } + + /** + * Overwrites the internal request logic to build the request message + * + * By overwriting this method, we can skip the actual request sending logic + * and still use the internal request serializing logic by accessing the + * given `$request` parameter and building our custom request object from + * it. We skip/ignore its parsing logic by returing an empty response here. + * This will implicitly be invoked by the call to `__soapCall()` in the + * above `encode()` method. + * + * @see SoapClient::__doRequest() + */ + public function __doRequest($request, $location, $action, $version, $one_way = 0) + { + $headers = array(); + if ($version === SOAP_1_1) { + $headers = array( + 'SOAPAction' => $action, + 'Content-Type' => 'text/xml; charset=utf-8' + ); + } elseif ($version === SOAP_1_2) { + $headers = array( + 'Content-Type' => 'application/soap+xml; charset=utf-8; action=' . $action + ); + } + + $this->request = new Request( + 'POST', + (string)$location, + $headers, + (string)$request + ); + + // do not actually block here, just pretend we're done... + return ''; + } +} diff --git a/vendor/clue/soap-react/src/Proxy.php b/vendor/clue/soap-react/src/Proxy.php new file mode 100644 index 0000000..06d70a0 --- /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 Proxy($client); + * ``` + * + * Each and every method call to the `Proxy` class will be sent via SOAP. + * + * ```php + * $proxy->myMethod($myArg1, $myArg2)->then(function ($response) { + * // result received + * }); + * ``` + * + * Please refer to your WSDL or its accompanying documentation for details + * on which functions and arguments are supported. + * + * > Note that this class is called "Proxy" because it will forward (proxy) all + * method calls to the actual SOAP service via the underlying + * [`Client::soapCall()`](#soapcall) method. This is not to be confused with + * using a proxy server. See [`Client`](#client) documentation for more + * details on how to use an HTTP proxy server. + */ +final class Proxy +{ + private $client; + + public function __construct(Client $client) + { + $this->client = $client; + } + + /** + * @param string $name + * @param mixed[] $args + * @return PromiseInterface + */ + public function __call($name, $args) + { + return $this->client->soapCall($name, $args); + } +} 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/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/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/composer.json b/vendor/clue/socks-react/composer.json new file mode 100644 index 0000000..c57742b --- /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": "^2.1 || ^1.2", + "react/socket": "^1.9" + }, + "require-dev": { + "clue/block-react": "^1.1", + "clue/connection-manager-extra": "^1.0 || ^0.7", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2", + "react/http": "^1.5" + }, + "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..5350b54 --- /dev/null +++ b/vendor/clue/socks-react/src/Client.php @@ -0,0 +1,452 @@ +<?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($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, $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 &$one) { + if (isset($one['args'])) { + foreach ($one['args'] as &$arg) { + if ($arg instanceof \Closure) { + $arg = '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..2405f3e --- /dev/null +++ b/vendor/clue/socks-react/src/Server.php @@ -0,0 +1,399 @@ +<?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, $auth = null) + { + if (\is_array($auth)) { + // wrap authentication array in authentication callback + $this->auth = function ($username, $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, $password, $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 ($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/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/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/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/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/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/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; + } +} |