diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e70ba4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,89 @@ +vendor/autoload.php +vendor/clue/ndjson-react/CHANGELOG.md +vendor/clue/ndjson-react/composer.json +vendor/clue/ndjson-react/LICENSE +vendor/clue/ndjson-react/README.md +vendor/clue/ndjson-react/.github/FUNDING.yml +vendor/clue/ndjson-react/src/Decoder.php +vendor/clue/ndjson-react/src/Encoder.php +vendor/clue/reactphp-sqlite/CHANGELOG.md +vendor/clue/reactphp-sqlite/composer.json +vendor/clue/reactphp-sqlite/LICENSE +vendor/clue/reactphp-sqlite/README.md +vendor/clue/reactphp-sqlite/.github/FUNDING.yml +vendor/clue/reactphp-sqlite/res/sqlite-worker.php +vendor/clue/reactphp-sqlite/src/DatabaseInterface.php +vendor/clue/reactphp-sqlite/src/Factory.php +vendor/clue/reactphp-sqlite/src/Result.php +vendor/clue/reactphp-sqlite/src/Io/BlockingDatabase.php +vendor/clue/reactphp-sqlite/src/Io/LazyDatabase.php +vendor/clue/reactphp-sqlite/src/Io/ProcessIoDatabase.php +vendor/composer/autoload_classmap.php +vendor/composer/autoload_files.php +vendor/composer/autoload_namespaces.php +vendor/composer/autoload_psr4.php +vendor/composer/autoload_real.php +vendor/composer/autoload_static.php +vendor/composer/ClassLoader.php +vendor/composer/installed.json +vendor/composer/installed.php +vendor/composer/InstalledVersions.php +vendor/composer/LICENSE +vendor/composer/platform_check.php +vendor/evenement/evenement/.gitattributes +vendor/evenement/evenement/composer.json +vendor/evenement/evenement/LICENSE +vendor/evenement/evenement/README.md +vendor/evenement/evenement/src/EventEmitter.php +vendor/evenement/evenement/src/EventEmitterInterface.php +vendor/evenement/evenement/src/EventEmitterTrait.php +vendor/react/child-process/CHANGELOG.md +vendor/react/child-process/composer.json +vendor/react/child-process/LICENSE +vendor/react/child-process/README.md +vendor/react/child-process/src/Process.php +vendor/react/event-loop/CHANGELOG.md +vendor/react/event-loop/composer.json +vendor/react/event-loop/LICENSE +vendor/react/event-loop/README.md +vendor/react/event-loop/src/ExtEventLoop.php +vendor/react/event-loop/src/ExtEvLoop.php +vendor/react/event-loop/src/ExtLibeventLoop.php +vendor/react/event-loop/src/ExtLibevLoop.php +vendor/react/event-loop/src/ExtUvLoop.php +vendor/react/event-loop/src/Factory.php +vendor/react/event-loop/src/Loop.php +vendor/react/event-loop/src/LoopInterface.php +vendor/react/event-loop/src/SignalsHandler.php +vendor/react/event-loop/src/StreamSelectLoop.php +vendor/react/event-loop/src/TimerInterface.php +vendor/react/event-loop/src/Tick/FutureTickQueue.php +vendor/react/event-loop/src/Timer/Timer.php +vendor/react/event-loop/src/Timer/Timers.php +vendor/react/promise/CHANGELOG.md +vendor/react/promise/composer.json +vendor/react/promise/LICENSE +vendor/react/promise/README.md +vendor/react/promise/src/Deferred.php +vendor/react/promise/src/functions_include.php +vendor/react/promise/src/functions.php +vendor/react/promise/src/Promise.php +vendor/react/promise/src/PromiseInterface.php +vendor/react/promise/src/Exception/CompositeException.php +vendor/react/promise/src/Exception/LengthException.php +vendor/react/promise/src/Internal/CancellationQueue.php +vendor/react/promise/src/Internal/FulfilledPromise.php +vendor/react/promise/src/Internal/RejectedPromise.php +vendor/react/stream/CHANGELOG.md +vendor/react/stream/composer.json +vendor/react/stream/LICENSE +vendor/react/stream/README.md +vendor/react/stream/src/CompositeStream.php +vendor/react/stream/src/DuplexResourceStream.php +vendor/react/stream/src/DuplexStreamInterface.php +vendor/react/stream/src/ReadableResourceStream.php +vendor/react/stream/src/ReadableStreamInterface.php +vendor/react/stream/src/ThroughStream.php +vendor/react/stream/src/Util.php +vendor/react/stream/src/WritableResourceStream.php +vendor/react/stream/src/WritableStreamInterface.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8d0cb42 --- /dev/null +++ b/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "clue/reactphp-sqlite": "^1.6", + "clue/framework-x": "^0.16" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..3d65051 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1123 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "db1acaba19a0c8b768f5023662d59dc1", + "packages": [ + { + "name": "clue/framework-x", + "version": "v0.16.0", + "source": { + "type": "git", + "url": "https://github.com/clue/framework-x.git", + "reference": "9b17ad9c86e7302c33d25ddc22ecfa9537a29c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/framework-x/zipball/9b17ad9c86e7302c33d25ddc22ecfa9537a29c6e", + "reference": "9b17ad9c86e7302c33d25ddc22ecfa9537a29c6e", + "shasum": "" + }, + "require": { + "nikic/fast-route": "^1.3", + "php": ">=7.1", + "react/async": "^4 || ^3", + "react/http": "^1.9", + "react/promise": "^3 || ^2.10", + "react/socket": "^1.13" + }, + "require-dev": { + "phpstan/phpstan": "1.10.47 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5", + "psr/container": "^2 || ^1", + "react/promise-timer": "^1.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "FrameworkX\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Framework X – the simple and fast micro framework for building reactive web applications that run anywhere.", + "homepage": "https://framework-x.org/", + "keywords": [ + "async", + "event-driven", + "framework", + "http", + "micro", + "microframework", + "reactphp", + "web" + ], + "support": { + "issues": "https://github.com/clue/framework-x/issues", + "source": "https://github.com/clue/framework-x/tree/v0.16.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2024-03-05T14:41:18+00:00" + }, + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "clue/reactphp-sqlite", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-sqlite.git", + "reference": "d670a6694e7c4ba327f4778c4f2fd499c3fb85fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-sqlite/zipball/d670a6694e7c4ba327f4778c4f2fd499c3fb85fc", + "reference": "d670a6694e7c4ba327f4778c4f2fd499c3fb85fc", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "ext-sqlite3": "*", + "php": ">=5.4", + "react/child-process": "^0.6", + "react/event-loop": "^1.2", + "react/promise": "^3 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\SQLite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Async SQLite database, lightweight non-blocking process wrapper around file-based database extension (ext-sqlite3), built on top of ReactPHP.", + "homepage": "https://github.com/clue/reactphp-sqlite", + "keywords": [ + "async", + "database", + "non-blocking", + "reactphp", + "sqlite" + ], + "support": { + "issues": "https://github.com/clue/reactphp-sqlite/issues", + "source": "https://github.com/clue/reactphp-sqlite/tree/v1.6.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-05-12T12:33:20+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "react/async", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/async.git", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.8 || ^1.2.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Async\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async utilities and fibers for ReactPHP", + "keywords": [ + "async", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/async/issues", + "source": "https://github.com/reactphp/async/tree/v4.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:40:02+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-09-16T13:41:56+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/http", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/http.git", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/http/zipball/8db02de41dcca82037367f67a2d4be365b1c4db9", + "reference": "8db02de41dcca82037367f67a2d4be365b1c4db9", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "php": ">=5.3.0", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" + }, + "require-dev": { + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.2 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": [ + "async", + "client", + "event-driven", + "http", + "http client", + "http server", + "https", + "psr-7", + "reactphp", + "server", + "streaming" + ], + "support": { + "issues": "https://github.com/reactphp/http/issues", + "source": "https://github.com/reactphp/http/tree/v1.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-11-20T15:24:08+00:00" + }, + { + "name": "react/promise", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", + "reference": "8a164643313c71354582dc850b42b33fa12a4b63", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-05-24T10:39:05+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/vendor/clue/framework-x/.github/FUNDING.yml b/vendor/clue/framework-x/.github/FUNDING.yml new file mode 100644 index 0000000..9c09fb8 --- /dev/null +++ b/vendor/clue/framework-x/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: clue +custom: https://clue.engineering/support diff --git a/vendor/clue/framework-x/CHANGELOG.md b/vendor/clue/framework-x/CHANGELOG.md new file mode 100644 index 0000000..157d446 --- /dev/null +++ b/vendor/clue/framework-x/CHANGELOG.md @@ -0,0 +1,373 @@ +# Changelog + +## 0.16.0 (2024-03-05) + +We are thrilled to announce the official release of `v0.16.0` to the public! 🎉🚀 +Additionally, we are making all previous tagged versions available to simplify the upgrade process. +In addition to the release of `v0.16.0`, this update includes all prior tagged releases. + +This release includes exciting new features such as improved performance, additional options +for access logging, updates to our documentation and nginx + Apache configurations, +as well as many more internal improvements to our test suite and integration tests. + +* Feature: Improve performance by skipping `AccessLogHandler` if it writes to `/dev/null`. + (#248 by @clue) + +* Feature: Add optional `$path` argument for `AccessLogHandler`. + (#247 by @clue) + +* Minor documentation improvements and update nginx + Apache configuration. + (#245 and #251 by @clue) + +* Improve test suite with improved directory structure for integration tests. + (#250 by @clue) + +## 0.15.0 (2023-12-07) + +* Feature: Full PHP 8.3 compatibility. + (#244 by @clue) + +* Feature: Add `App::__invoke()` method to enable custom integrations. + (#236 by @clue) + +* Feature: Improve performance by only using `FiberHandler` for `ReactiveHandler`. + (#237 by @clue) + +* Minor documentation improvements. + (#242 by @yadaiio) + +## 0.14.0 (2023-07-31) + +* Feature: Improve Promise v3 support and use Promise v3 template types. + (#233 and #235 by @clue) + +* Feature: Improve handling `OPTIONS *` requests. + (#226 by @clue) + +* Refactor logging into new `LogStreamHandler` and reactive server logic into new `ReactiveHandler`. + (#222 and #224 by @clue) + +* Improve test suite and ensure 100% code coverage. + (#217, #221, #225 and #228 by @clue) + +## 0.13.0 (2023-02-22) + +* Feature: Forward compatibility with upcoming Promise v3. + (#188 by @clue) + +* Feature: Full PHP 8.2 compatibility. + (#194 and #207 by @clue) + +* Feature: Load environment variables from `$_ENV`, `$_SERVER` and `getenv()`. + (#205 by @clue) + +* Feature: Update to support `Content-Length` response header on `HEAD` requests. + (#186 by @clue) + +* Feature / Fix: Consistent handling for HTTP responses with multiple header values (PHP SAPI). + (#214 by @pfk84) + +* Fix: Respect explicit response status code when Location response header is given (PHP SAPI). + (#191 by @jkrzefski) + +* Minor documentation improvements. + (#189 by @clue) + +* Add PHPStan to test environment on level `max` and improve type definitions. + (#200, #201 and #204 by @clue) + +* Improve test suite and report failed assertions. + (#199 by @clue and #208 by @SimonFrings) + +## 0.12.0 (2022-08-03) + +* Feature: Support loading environment variables from DI container configuration. + (#184 by @clue) + +* Feature: Support typed container variables for container factory functions. + (#178, #179 and #180 by @clue) + +* Feature: Support nullable and `null` arguments and default values for DI container configuration. + (#181 and #183 by @clue) + +* Feature: Support untyped and `mixed` arguments for container factory. + (#182 by @clue) + +## 0.11.0 (2022-07-26) + +* Feature: Make `AccessLogHandler` and `ErrorHandler` part of public API. + (#173 and #174 by @clue) + + ```php + run(); + ``` + +* Feature: Support loading `AccessLogHandler` and `ErrorHandler` from `Container`. + (#175 by @clue) + +* Feature: Read `$remote_addr` attribute for `AccessLogHandler` (trusted proxies). + (#177 by @clue) + +* Internal refactoring to move all handlers to `Io` namespace. + (#176 by @clue) + +* Update test suite to remove deprecated `utf8_decode()` (PHP 8.2 preparation). + (#171 by SimonFrings) + +## 0.10.0 (2022-07-14) + +* Feature: Built-in support for fibers on PHP 8.1+ with stable reactphp/async. + (#168 by @clue) + + ```php + $app->get('/book/{isbn}', function (Psr\Http\Message\ServerRequestInterface $request) use ($db) { + $isbn = $request->getAttribute('isbn'); + $result = await($db->query( + 'SELECT title FROM book WHERE isbn = ?', + [$isbn] + )); + + assert($result instanceof React\MySQL\QueryResult); + $data = $result->resultRows[0]['title']; + + return React\Http\Message\Response::plaintext( + $data + ); + }); + ``` + +* Feature: Support PSR-11 container interface by using DI container as adapter. + (#163 by @clue) + +* Minor documentation improvements. + (#158 by @clue and #160 by @SimonFrings) + +## 0.9.0 (2022-05-13) + +* Feature: Add signal handling support for `SIGINT` and `SIGTERM`. + (#150 by @clue) + +* Feature: Improve error output for exception messages with special characters. + (#131 by @clue) + +* Add new documentation chapters for Docker containers and HTTP redirecting. + (#138 by SimonFrings and #136, #151 and #156 by @clue) + +* Minor documentation improvements. + (#143 by @zf2timo, #153 by @mattschlosser and #129 and #154 by @clue) + +* Improve test suite and add tests for `Dockerfile` instructions. + (#148 and #149 by @clue) + +## 0.8.0 (2022-03-07) + +* Feature: Automatically start new fiber for each request on PHP 8.1+. + (#117 by @clue) + +* Feature: Add fiber compatibility mode for PHP < 8.1. + (#128 by @clue) + +* Improve documentation and update installation instructions for react/async. + (#116 and #126 by @clue and #124, #125 and #127 by @SimonFrings) + +* Improve fiber tests to avoid now unneeded `await()` calls. + (#118 by @clue) + +## 0.7.0 (2022-02-05) + +* Feature: Update to use HTTP status code constants and JSON/HTML response helpers. + (#114 by @clue) + + ```php + $app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello " . $request->getAttribute('name') . "!\n" + ); + }); + ``` + +* Feature / Fix: Update to improve protocol handling for HTTP responses with no body. + (#113 by @clue) + +* Minor documentation improvements. + (#112 by @SimonFrings and #115 by @netcarver) + +## 0.6.0 (2021-12-20) + +* Feature: Support automatic dependency injection by using class names (DI container). + (#89, #92 and #94 by @clue) + + ```php + get('/', Acme\Todo\HelloController::class); + $app->get('/users/{name}', Acme\Todo\UserController::class); + + $app->run(); + ``` + +* Feature: Add support for explicit DI container configuration. + (#95, #96 and #97 by @clue) + + ```php + fn() => new Acme\Todo\HelloController(); + Acme\Todo\UserController::class => function (React\Http\Browser $browser) { + // example UserController class requires two arguments: + // - first argument will be autowired based on class reference + // - second argument expects some manual value + return new Acme\Todo\UserController($browser, 42); + } + ]); + + // … + ``` + +* Feature: Refactor to use `$_SERVER` instead of `getenv()`. + (#91 by @bpolaszek) + +* Minor documentation improvements. + (#100 by @clue) + +* Update test suite to use stable PHP 8.1 Docker image. + (#90 by @clue) + +## 0.5.0 (2021-11-30) + +* Feature / BC break: Simplify `App` by always using default loop, drop optional loop instance. + (#88 by @clue) + + ```php + // old + $loop = React\EventLoop\Loop::get(); + $app = new FrameworkX\App($loop); + + // new (already supported before) + $app = new FrameworkX\App(); + ``` + +* Add documentation for manual restart of systemd service and chapter for Caddy deployment. + (#87 by @SimonFrings and #82 by @francislavoie) + +* Improve documentation, remove leftover `$loop` references and fix typos. + (#72 by @shuvroroy, #80 by @Ivanshamir, #81 by @clue and #83 by @rattuscz) + +## 0.4.0 (2021-11-23) + +We are excited to announce the official release of Framework X to the public! 🎉🚀 +This release includes exciting new features such as full compatibility with PHP 8.1, +improvements to response handling, and enhanced documentation covering nginx, +Apache, and async database usage. + +* Feature: Announce Framework X public beta. + (#64 by @clue) + +* Feature: Full PHP 8.1 compatibility. + (#58 by @clue) + +* Feature: Improve `AccessLogHandler` and fix response size for streaming response body. + (#47, #48, #49 and #50 by @clue) + +* Feature / Fix: Skip sending body and `Content-Length` for responses with no body. + (#51 by @clue) + +* Feature / Fix: Consistently reject proxy requests and handle `OPTIONS *` requests. + (#46 by @clue) + +* Add new documentation chapters for nginx, Apache and async database. + (#57, #59 and #60 by @clue) + +* Improve documentation, examples and describe HTTP caching and output buffering. + (#52, #53, #55, #56, #61, #62 and #63 by @clue) + +## 0.3.0 (2021-09-23) + +* Feature: Add support for global middleware. + (#23 by @clue) + +* Feature: Improve error output and refactor internal error handler. + (#37, #39 and #41 by @clue) + +* Feature: Support changing listening address via new `X_LISTEN` environment variable. + (#38 by @clue) + +* Feature: Update to new ReactPHP HTTP and Socket API. + (#26 and #29 by @HLeithner and #34 by @clue) + +* Feature: Refactor to use new `AccessLogHandler`, `RouteHandler`, `RedirectHandler` and `SapiHandler`. + (#42, #43, #44 and #45 by @clue) + +* Fix: Fix path filter regex. + (#27 by @HLeithner) + +* Add documentation for async middleware and systemd service unit configuration. + (#24 by @Degra1991 and #32, #35, #36 and #40 by @clue) + +* Improve test suite and run tests on Windows with PHPUnit. + (#31 by @SimonFrings and #28 and #33 by @clue) + +## 0.2.0 (2021-06-18) + +* Feature: Simplify `App` usage by making `LoopInterface` argument optional. + (#22 by @clue) + + ```php + // old (still supported) + $loop = React\EventLoop\Factory::create(); + $app = new FrameworkX\App($loop); + + // new (using default loop) + $app = new FrameworkX\App(); + ``` + +* Feature: Add middleware support. + (#18 by @clue) + +* Feature: Refactor and simplify route dispatcher. + (#21 by @clue) + +* Feature: Add Generator-based coroutine implementation. + (#17 by @clue) + +* Minor documentation improvements. + (#15, #16 and #19 by @clue) + +## 0.1.0 (2021-04-30) + +We're excited to announce the release of the first version of Framework X in +private beta! This version marks the starting point of our project and is the +first of many milestones for making async PHP easier than ever before. + +* Release Framework X, major documentation overhaul and improve examples. + (#14, #13 and #2 by @clue) + +* Feature: Support running behind nginx and Apache (PHP-FPM and mod_php). + (#3, #11 and #12 by @clue) + +* Feature / Fix: Consistently parse request URI and improve URL handling. + (#4, #5, #6 and #7 by @clue) + +* Feature: Rewrite `FilesystemHandler`, improve file access and directory listing. + (#8 and #9 by @clue) + +* Feature: Add `any()` router method to match any request method. + (#10 by @clue) diff --git a/vendor/clue/framework-x/LICENSE b/vendor/clue/framework-x/LICENSE new file mode 100644 index 0000000..d8337e7 --- /dev/null +++ b/vendor/clue/framework-x/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 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/framework-x/README.md b/vendor/clue/framework-x/README.md new file mode 100644 index 0000000..78a2686 --- /dev/null +++ b/vendor/clue/framework-x/README.md @@ -0,0 +1,147 @@ +# Framework X + +[![CI status](https://github.com/clue-access/framework-x/workflows/CI/badge.svg)](https://github.com/clue-access/framework-x/actions) +[![code coverage](https://img.shields.io/badge/code%20coverage-100%25-success)](#tests) + +Framework X – the simple and fast micro framework for building reactive web applications that run anywhere. + +* [Support us](#support-us) +* [Quickstart](#quickstart) +* [Documentation](#documentation) +* [Contribute](#contribute) +* [Tests](#tests) +* [License](#license) + +## Support us + +We invest a lot of time developing, maintaining and updating our awesome +open-source projects. You can help us sustain this high-quality of our work by +[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get +numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) +for details. + +Let's take these projects to the next level together! 🚀 + +## Quickstart + +Start by creating an empty project directory. +Next, we can start by taking a look at a simple example application. +You can use this example to get started by creating a new `public/` directory with +an `index.php` file inside: + +```php +get('/', function () { + return React\Http\Message\Response::plaintext( + "Hello wörld!\n" + ); +}); + +$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello " . $request->getAttribute('name') . "!\n" + ); +}); + +$app->run(); +``` + +Next, we need to install X and its dependencies to actually run this project. +In your project directory, simply run the following command: + +```bash +$ composer require clue/framework-x:^0.16 +``` + +> See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +That's it already! The next step is now to serve this web application. +One of the nice properties of this project is that is works both behind +traditional web server setups as well as in a stand-alone environment. + +For example, you can run the above example using the built-in web server like +this: + +```bash +$ php public/index.php +``` + +You can now use your favorite web browser or command line tool to check your web +application responds as expected: + +```bash +$ curl http://localhost:8080/ +Hello wörld! +``` + +## Documentation + +Hooked? +See [website](https://framework-x.org/) for full documentation. + +Found a typo or want to contribute? +The website documentation is built from the source documentation files in +the [docs/](docs/) folder. + +## Contribute + +You want to contribute to the Framework X source code or documentation? You've +come to the right place! + +To contribute to the source code just locate the [src/](src/) folder and you'll find all +content in there. Additionally, our [tests/](tests/) folder contains all our unit +tests and acceptance tests to assure our code works as expected. For more +information on how to run the test suite check out our [testing chapter](#tests). + +If you want to contribute to the [documentation](#documentation) of Framework X +found on the website, take a look inside the [docs/](docs/) folder. You'll find further +instructions inside the `README.md` in there. + +Found a typo on our [website](https://framework-x.org/)? Simply go to our +[website repository](https://github.com/clue/framework-x-website) +and follow the instructions found in the `README`. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +$ composer install +``` + +To run the test suite, go to the project root and run: + +```bash +$ vendor/bin/phpunit +``` + +The test suite is set up to always ensure 100% code coverage across all +supported environments. If you have the Xdebug extension installed, you can also +generate a code coverage report locally like this: + +```bash +$ XDEBUG_MODE=coverage vendor/bin/phpunit --coverage-text +``` + +Additionally, you can run our sophisticated integration tests to verify the +framework examples work as expected behind your web server. Use your web server +of choice (see deployment documentation) and execute the tests with the URL to +your installation like this: + +```bash +$ php tests/integration/public/index.php +$ tests/integration.bash http://localhost:8080 +``` + +## License + +This project is released under the permissive [MIT license](LICENSE). + +> Did you know that I offer custom development services and issuing invoices for + sponsorships of releases and for contributions? Contact me (@clue) for details. diff --git a/vendor/clue/framework-x/composer.json b/vendor/clue/framework-x/composer.json new file mode 100644 index 0000000..e41dcb5 --- /dev/null +++ b/vendor/clue/framework-x/composer.json @@ -0,0 +1,40 @@ +{ + "name": "clue/framework-x", + "description": "Framework X – the simple and fast micro framework for building reactive web applications that run anywhere.", + "keywords": ["microframework", "micro", "framework", "web", "http", "event-driven", "async", "ReactPHP"], + "homepage": "https://framework-x.org/", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "require": { + "php": ">=7.1", + "nikic/fast-route": "^1.3", + "react/async": "^4 || ^3", + "react/http": "^1.9", + "react/promise": "^3 || ^2.10", + "react/socket": "^1.13" + }, + "require-dev": { + "phpstan/phpstan": "1.10.47 || 1.4.10", + "phpunit/phpunit": "^9.6 || ^7.5", + "psr/container": "^2 || ^1", + "react/promise-timer": "^1.10" + }, + "autoload": { + "psr-4": { + "FrameworkX\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "FrameworkX\\Tests\\": "tests/" + }, + "files": [ + "tests/FiberStub.php" + ] + } +} diff --git a/vendor/clue/framework-x/src/AccessLogHandler.php b/vendor/clue/framework-x/src/AccessLogHandler.php new file mode 100644 index 0000000..a5d87f4 --- /dev/null +++ b/vendor/clue/framework-x/src/AccessLogHandler.php @@ -0,0 +1,140 @@ +isDevNull()) { + // only assign logger if we're not logging to /dev/null (which would discard any logs) + $this->logger = $logger; + } + + $this->hasHighResolution = \function_exists('hrtime'); // PHP 7.3+ + } + + /** + * [Internal] Returns whether we're writing to /dev/null (which will discard any logs) + * + * @internal + * @return bool + */ + public function isDevNull(): bool + { + return $this->logger === null; + } + + /** + * @return ResponseInterface|PromiseInterface|\Generator + */ + public function __invoke(ServerRequestInterface $request, callable $next) + { + if ($this->logger === null) { + // Skip if we're logging to /dev/null (which will discard any logs). + // As an additional optimization, the `App` will automatically + // detect we no longer need to invoke this instance at all. + return $next($request); // @codeCoverageIgnore + } + + $now = $this->now(); + $response = $next($request); + + if ($response instanceof PromiseInterface) { + /** @var PromiseInterface $response */ + return $response->then(function (ResponseInterface $response) use ($request, $now) { + $this->logWhenClosed($request, $response, $now); + return $response; + }); + } elseif ($response instanceof \Generator) { + return (function (\Generator $generator) use ($request, $now) { + $response = yield from $generator; + $this->logWhenClosed($request, $response, $now); + return $response; + })($response); + } else { + $this->logWhenClosed($request, $response, $now); + return $response; + } + } + + /** + * checks if response body is closed (not streaming) before writing log message for response + */ + private function logWhenClosed(ServerRequestInterface $request, ResponseInterface $response, float $start): void + { + $body = $response->getBody(); + + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $size = 0; + $body->on('data', function (string $chunk) use (&$size) { + $size += strlen($chunk); + }); + + $body->on('close', function () use (&$size, $request, $response, $start) { + $this->log($request, $response, $size, $this->now() - $start); + }); + } else { + $this->log($request, $response, $body->getSize() ?? strlen((string) $body), $this->now() - $start); + } + } + + /** + * writes log message for response after response body is closed (not streaming anymore) + */ + private function log(ServerRequestInterface $request, ResponseInterface $response, int $responseSize, float $time): void + { + $method = $request->getMethod(); + $status = $response->getStatusCode(); + + // HEAD requests and `204 No Content` and `304 Not Modified` always use an empty response body + if ($method === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) { + $responseSize = 0; + } + + \assert($this->logger instanceof LogStreamHandler); + $this->logger->log( + ($request->getAttribute('remote_addr') ?? $request->getServerParams()['REMOTE_ADDR'] ?? '-') . ' ' . + '"' . $this->escape($method) . ' ' . $this->escape($request->getRequestTarget()) . ' HTTP/' . $request->getProtocolVersion() . '" ' . + $status . ' ' . $responseSize . ' ' . sprintf('%.3F', $time < 0 ? 0 : $time) + ); + } + + private function escape(string $s): string + { + return (string) preg_replace_callback('/[\x00-\x1F\x7F-\xFF"\\\\]+/', function (array $m) { + return str_replace('%', '\x', rawurlencode($m[0])); + }, $s); + } + + private function now(): float + { + return $this->hasHighResolution ? hrtime(true) * 1e-9 : microtime(true); + } +} diff --git a/vendor/clue/framework-x/src/App.php b/vendor/clue/framework-x/src/App.php new file mode 100644 index 0000000..2e000b2 --- /dev/null +++ b/vendor/clue/framework-x/src/App.php @@ -0,0 +1,354 @@ +getAccessLogHandler(); + } elseif ($handler === ErrorHandler::class) { + $handler = $container->getErrorHandler(); + } + + // ensure AccessLogHandler is always followed by ErrorHandler + if ($needsErrorHandlerNext && !$handler instanceof ErrorHandler) { + break; + } + $needsErrorHandlerNext = false; + + if ($handler instanceof Container) { + // remember last Container to load any following class names + $container = $handler; + + // add default ErrorHandler from last Container before adding any other handlers, may be followed by other Container instances (unlikely) + if (!$handlers) { + $needsErrorHandler = $needsAccessLog = $container; + } + } elseif (!\is_callable($handler)) { + $handlers[] = $container->callable($handler); + } else { + // don't need a default ErrorHandler if we're adding one as first handler or AccessLogHandler as first followed by one + if ($needsErrorHandler && ($handler instanceof ErrorHandler || $handler instanceof AccessLogHandler) && !$handlers) { + $needsErrorHandler = null; + } + + // only add to list of handlers if this is not a NOOP + if (!$handler instanceof AccessLogHandler || !$handler->isDevNull()) { + $handlers[] = $handler; + } + + if ($handler instanceof AccessLogHandler) { + $needsAccessLog = null; + $needsErrorHandlerNext = true; + } + } + } + if ($needsErrorHandlerNext) { + throw new \TypeError('AccessLogHandler must be followed by ErrorHandler'); + } + } + + // add default ErrorHandler as first handler unless it is already added explicitly + if ($needsErrorHandler instanceof Container) { + \array_unshift($handlers, $needsErrorHandler->getErrorHandler()); + } + + // only log for built-in webserver and PHP development webserver by default, others have their own access log + if ($needsAccessLog instanceof Container) { + $handler = $needsAccessLog->getAccessLogHandler(); + if (!$handler->isDevNull()) { + \array_unshift($handlers, $handler); + } + } + + $this->router = new RouteHandler($container); + $handlers[] = $this->router; + $this->handler = new MiddlewareHandler($handlers); + $this->sapi = \PHP_SAPI === 'cli' ? new ReactiveHandler($container->getEnv('X_LISTEN')) : new SapiHandler(); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function get(string $route, $handler, ...$handlers): void + { + $this->map(['GET'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function head(string $route, $handler, ...$handlers): void + { + $this->map(['HEAD'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function post(string $route, $handler, ...$handlers): void + { + $this->map(['POST'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function put(string $route, $handler, ...$handlers): void + { + $this->map(['PUT'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function patch(string $route, $handler, ...$handlers): void + { + $this->map(['PATCH'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function delete(string $route, $handler, ...$handlers): void + { + $this->map(['DELETE'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function options(string $route, $handler, ...$handlers): void + { + // backward compatibility: `OPTIONS * HTTP/1.1` can be matched with empty path (legacy) + if ($route === '') { + $route = '*'; + } + + $this->map(['OPTIONS'], $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function any(string $route, $handler, ...$handlers): void + { + $this->map(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], $route, $handler, ...$handlers); + } + + /** + * + * @param string[] $methods + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function map(array $methods, string $route, $handler, ...$handlers): void + { + $this->router->map($methods, $route, $handler, ...$handlers); + } + + /** + * @param string $route + * @param string $target + * @param int $code + */ + public function redirect(string $route, string $target, int $code = Response::STATUS_FOUND): void + { + $this->any($route, new RedirectHandler($target, $code)); + } + + /** + * Runs the app to handle HTTP requests according to any registered routes and middleware. + * + * This is where the magic happens: When executed on the command line (CLI), + * this will run the powerful reactive request handler built on top of + * ReactPHP. This works by running the efficient built-in HTTP web server to + * handle incoming HTTP requests through ReactPHP's HTTP and socket server. + * This async execution mode is usually recommended as it can efficiently + * process a large number of concurrent connections and process multiple + * incoming requests simultaneously. The long-running server process will + * continue to run until it is interrupted by a signal. + * + * When executed behind traditional PHP SAPIs (PHP-FPM, FastCGI, Apache, etc.), + * this will handle a single request and run until a single response is sent. + * This is particularly useful because it allows you to run the exact same + * app in any environment. + * + * @see ReactiveHandler::run() + * @see SapiHandler::run() + */ + public function run(): void + { + $this->sapi->run(\Closure::fromCallable([$this, 'handleRequest'])); + } + + /** + * Invokes the app to handle a single HTTP request according to any registered routes and middleware. + * + * This method allows you to pass in a single HTTP request object that will + * be processed according to any registered routes and middleware and will + * return an HTTP response object as a result. + * + * ```php + * $app = new FrameworkX\App(); + * $app->get('/', fn() => React\Http\Message\Response::plaintext("Hello!\n")); + * + * $request = new React\Http\Message\ServerRequest('GET', 'https://example.com/'); + * $response = $app($request); + * + * assert($response instanceof Psr\Http\Message\ResponseInterface); + * assert($response->getStatusCode() === 200); + * assert($response->getBody()->getContents() === "Hello\n"); + * ``` + * + * This is particularly useful for higher-level integration test suites and + * for custom integrations with other runtime environments like serverless + * functions or other frameworks. Otherwise, most applications would likely + * want to use the `run()` method to run the application and automatically + * accept incoming HTTP requests according to the PHP SAPI in use. + * + * @param ServerRequestInterface $request The HTTP request object to process. + * @return ResponseInterface This method returns an HTTP response object + * according to any registered routes and middleware. If any handler is + * async, it will await its execution before returning, running the + * event loop as needed. If the request can not be routed or any handler + * fails, it will return a matching HTTP error response object. + * @throws void This method never throws. If the request can not be routed + * or any handler fails, it will be turned into a valid error response + * before returning. + * @see self::run() + */ + public function __invoke(ServerRequestInterface $request): ResponseInterface + { + $response = $this->handleRequest($request); + if ($response instanceof PromiseInterface) { + /** @throws void */ + $response = await($response); + assert($response instanceof ResponseInterface); + } + + return $response; + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface|PromiseInterface + * Returns a response or a Promise which eventually fulfills with a + * response. This method never throws or resolves a rejected promise. + * If the request can not be routed or the handler fails, it will be + * turned into a valid error response before returning. + * @throws void + */ + private function handleRequest(ServerRequestInterface $request) + { + $response = ($this->handler)($request); + assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator); + + if ($response instanceof \Generator) { + if ($response->valid()) { + $response = $this->coroutine($response); + } else { + $response = $response->getReturn(); + assert($response instanceof ResponseInterface); + } + } + + return $response; + } + + /** + * @return PromiseInterface + */ + private function coroutine(\Generator $generator): PromiseInterface + { + $next = null; + $deferred = new Deferred(); + $next = function () use ($generator, &$next, $deferred) { + if (!$generator->valid()) { + $deferred->resolve($generator->getReturn()); + return; + } + + $promise = $generator->current(); + assert($promise instanceof PromiseInterface); + + $promise->then(function ($value) use ($generator, $next) { + $generator->send($value); + $next(); + }, function ($reason) use ($generator, $next) { + $generator->throw($reason); + $next(); + }); + }; + + $next(); + + return $deferred->promise(); + } +} diff --git a/vendor/clue/framework-x/src/Container.php b/vendor/clue/framework-x/src/Container.php new file mode 100644 index 0000000..2058379 --- /dev/null +++ b/vendor/clue/framework-x/src/Container.php @@ -0,0 +1,382 @@ +|ContainerInterface */ + private $container; + + /** @var bool */ + private $useProcessEnv; + + /** @param array|ContainerInterface $loader */ + public function __construct($loader = []) + { + /** @var mixed $loader explicit type check for mixed if user ignores parameter type */ + if (!\is_array($loader) && !$loader instanceof ContainerInterface) { + throw new \TypeError( + 'Argument #1 ($loader) must be of type array|Psr\Container\ContainerInterface, ' . (\is_object($loader) ? get_class($loader) : gettype($loader)) . ' given' + ); + } + + foreach (($loader instanceof ContainerInterface ? [] : $loader) as $name => $value) { + if ( + (!\is_object($value) && !\is_scalar($value) && $value !== null) || + (!$value instanceof $name && !$value instanceof \Closure && !\is_string($value) && \strpos($name, '\\') !== false) + ) { + throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (is_object($value) ? get_class($value) : gettype($value))); + } + } + $this->container = $loader; + + // prefer reading environment from `$_ENV` and `$_SERVER`, only fall back to `getenv()` in thread-safe environments + $this->useProcessEnv = \ZEND_THREAD_SAFE === false || \in_array(\PHP_SAPI, ['cli', 'cli-server', 'cgi-fcgi', 'fpm-fcgi'], true); + } + + /** @return mixed */ + public function __invoke(ServerRequestInterface $request, callable $next = null) + { + if ($next === null) { + // You don't want to end up here. This only happens if you use the + // container as a final request handler instead of as a middleware. + // In this case, you should omit the container or add another final + // request handler behind the container in the middleware chain. + throw new \BadMethodCallException('Container should not be used as final request handler'); + } + + // If the container is used as a middleware, simply forward to the next + // request handler. As an additional optimization, the container would + // usually be filtered out from a middleware chain as this is a NO-OP. + return $next($request); + } + + /** + * @param class-string $class + * @return callable(ServerRequestInterface,?callable=null) + * @internal + */ + public function callable(string $class): callable + { + return function (ServerRequestInterface $request, callable $next = null) use ($class) { + // Check `$class` references a valid class name that can be autoloaded + if (\is_array($this->container) && !\class_exists($class, true) && !interface_exists($class, false) && !trait_exists($class, false)) { + throw new \BadMethodCallException('Request handler class ' . $class . ' not found'); + } + + try { + if ($this->container instanceof ContainerInterface) { + $handler = $this->container->get($class); + } else { + $handler = $this->loadObject($class); + } + } catch (\Throwable $e) { + throw new \BadMethodCallException( + 'Request handler class ' . $class . ' failed to load: ' . $e->getMessage(), + 0, + $e + ); + } + + // Check `$handler` references a class name that is callable, i.e. has an `__invoke()` method. + // This initial version is intentionally limited to checking the method name only. + // A follow-up version will likely use reflection to check request handler argument types. + if (!is_callable($handler)) { + throw new \BadMethodCallException('Request handler class "' . $class . '" has no public __invoke() method'); + } + + // invoke request handler as middleware handler or final controller + if ($next === null) { + return $handler($request); + } + return $handler($request, $next); + }; + } + + /** @internal */ + public function getEnv(string $name): ?string + { + assert(\preg_match('/^[A-Z][A-Z0-9_]+$/', $name) === 1); + + if ($this->container instanceof ContainerInterface && $this->container->has($name)) { + $value = $this->container->get($name); + } elseif ($this->hasVariable($name)) { + $value = $this->loadVariable($name, 'mixed', true, 64); + } else { + return null; + } + + if (!\is_string($value) && $value !== null) { + throw new \TypeError('Environment variable $' . $name . ' expected type string|null, but got ' . (\is_object($value) ? \get_class($value) : \gettype($value))); + } + + return $value; + } + + /** @internal */ + public function getAccessLogHandler(): AccessLogHandler + { + if ($this->container instanceof ContainerInterface) { + if ($this->container->has(AccessLogHandler::class)) { + // @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError` + return $this->container->get(AccessLogHandler::class); + } else { + return new AccessLogHandler(); + } + } + return $this->loadObject(AccessLogHandler::class); + } + + /** @internal */ + public function getErrorHandler(): ErrorHandler + { + if ($this->container instanceof ContainerInterface) { + if ($this->container->has(ErrorHandler::class)) { + // @phpstan-ignore-next-line method return type will ensure correct type or throw `TypeError` + return $this->container->get(ErrorHandler::class); + } else { + return new ErrorHandler(); + } + } + return $this->loadObject(ErrorHandler::class); + } + + /** + * @template T of object + * @param class-string $name + * @return T + * @throws \BadMethodCallException if object of type $name can not be loaded + */ + private function loadObject(string $name, int $depth = 64) /*: object (PHP 7.2+) */ + { + assert(\is_array($this->container)); + + if (\array_key_exists($name, $this->container)) { + if (\is_string($this->container[$name])) { + if ($depth < 1) { + throw new \BadMethodCallException('Factory for ' . $name . ' is recursive'); + } + + // @phpstan-ignore-next-line because type of container value is explicitly checked after getting here + $value = $this->loadObject($this->container[$name], $depth - 1); + if (!$value instanceof $name) { + throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . \get_class($value)); + } + + $this->container[$name] = $value; + } elseif ($this->container[$name] instanceof \Closure) { + // build list of factory parameters based on parameter types + $closure = new \ReflectionFunction($this->container[$name]); + $params = $this->loadFunctionParams($closure, $depth, true); + + // invoke factory with list of parameters + $value = $params === [] ? ($this->container[$name])() : ($this->container[$name])(...$params); + + if (\is_string($value)) { + if ($depth < 1) { + throw new \BadMethodCallException('Factory for ' . $name . ' is recursive'); + } + + // @phpstan-ignore-next-line because type of container value is explicitly checked after getting here + $value = $this->loadObject($value, $depth - 1); + } + if (!$value instanceof $name) { + throw new \BadMethodCallException('Factory for ' . $name . ' returned unexpected ' . (is_object($value) ? get_class($value) : gettype($value))); + } + + $this->container[$name] = $value; + } elseif (!$this->container[$name] instanceof $name) { + throw new \BadMethodCallException('Map for ' . $name . ' contains unexpected ' . (\is_object($this->container[$name]) ? \get_class($this->container[$name]) : \gettype($this->container[$name]))); + } + + assert($this->container[$name] instanceof $name); + + return $this->container[$name]; + } + + // Check `$name` references a valid class name that can be autoloaded + if (!\class_exists($name, true) && !interface_exists($name, false) && !trait_exists($name, false)) { + throw new \BadMethodCallException('Class ' . $name . ' not found'); + } + + $class = new \ReflectionClass($name); + if (!$class->isInstantiable()) { + $modifier = 'class'; + if ($class->isInterface()) { + $modifier = 'interface'; + } elseif ($class->isAbstract()) { + $modifier = 'abstract class'; + } elseif ($class->isTrait()) { + $modifier = 'trait'; + } + throw new \BadMethodCallException('Cannot instantiate ' . $modifier . ' '. $name); + } + + // build list of constructor parameters based on parameter types + $ctor = $class->getConstructor(); + $params = $ctor === null ? [] : $this->loadFunctionParams($ctor, $depth, false); + + // instantiate with list of parameters + // @phpstan-ignore-next-line because `$class->newInstance()` is known to return `T` + return $this->container[$name] = $params === [] ? new $name() : $class->newInstance(...$params); + } + + /** + * @return list + * @throws \BadMethodCallException if either parameter can not be loaded + */ + private function loadFunctionParams(\ReflectionFunctionAbstract $function, int $depth, bool $allowVariables): array + { + $params = []; + foreach ($function->getParameters() as $parameter) { + $params[] = $this->loadParameter($parameter, $depth, $allowVariables); + } + + return $params; + } + + /** + * @return mixed + * @throws \BadMethodCallException if $parameter can not be loaded + */ + private function loadParameter(\ReflectionParameter $parameter, int $depth, bool $allowVariables) /*: mixed (PHP 8.0+) */ + { + assert(\is_array($this->container)); + + $type = $parameter->getType(); + $hasDefault = $parameter->isDefaultValueAvailable() || ((!$type instanceof \ReflectionNamedType || $type->getName() !== 'mixed') && $parameter->allowsNull()); + + // abort for union types (PHP 8.0+) and intersection types (PHP 8.1+) + // @phpstan-ignore-next-line for PHP < 8 + if ($type instanceof \ReflectionUnionType || $type instanceof \ReflectionIntersectionType) { // @codeCoverageIgnoreStart + if ($hasDefault) { + return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + } + throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type); + } // @codeCoverageIgnoreEnd + + // load container variables if parameter name is known + assert($type === null || $type instanceof \ReflectionNamedType); + if ($allowVariables && $this->hasVariable($parameter->getName())) { + return $this->loadVariable($parameter->getName(), $type === null ? 'mixed' : $type->getName(), $parameter->allowsNull(), $depth); + } + + // abort if parameter is untyped and not explicitly defined by container variable + if ($type === null) { + assert($parameter->allowsNull()); + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + throw new \BadMethodCallException(self::parameterError($parameter) . ' has no type'); + } + + // use default/nullable argument if not loadable as container variable or by type + assert($type instanceof \ReflectionNamedType); + if ($hasDefault && ($type->isBuiltin() || !\array_key_exists($type->getName(), $this->container))) { + return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + } + + // abort if required container variable is not defined or for any other primitive types (array etc.) + if ($type->isBuiltin()) { + if ($allowVariables) { + throw new \BadMethodCallException(self::parameterError($parameter) . ' is not defined'); + } else { + throw new \BadMethodCallException(self::parameterError($parameter) . ' expects unsupported type ' . $type->getName()); + } + } + + // abort for unreasonably deep nesting or recursive types + if ($depth < 1) { + throw new \BadMethodCallException(self::parameterError($parameter) . ' is recursive'); + } + + // @phpstan-ignore-next-line because `$type->getName()` is a `class-string` by definition + return $this->loadObject($type->getName(), $depth - 1); + } + + private function hasVariable(string $name): bool + { + return (\is_array($this->container) && \array_key_exists($name, $this->container)) || (isset($_ENV[$name]) || (\is_string($_SERVER[$name] ?? null) || ($this->useProcessEnv && \getenv($name) !== false)) && \preg_match('/^[A-Z][A-Z0-9_]+$/', $name)); + } + + /** + * @return object|string|int|float|bool|null + * @throws \BadMethodCallException if $name is not a valid container variable + */ + private function loadVariable(string $name, string $type, bool $nullable, int $depth) /*: object|string|int|float|bool|null (PHP 8.0+) */ + { + assert($this->hasVariable($name)); + assert(\is_array($this->container) || !$this->container->has($name)); + + if (\is_array($this->container) && ($this->container[$name] ?? null) instanceof \Closure) { + if ($depth < 1) { + throw new \BadMethodCallException('Container variable $' . $name . ' is recursive'); + } + + // build list of factory parameters based on parameter types + $factory = $this->container[$name]; + assert($factory instanceof \Closure); + $closure = new \ReflectionFunction($factory); + $params = $this->loadFunctionParams($closure, $depth - 1, true); + + // invoke factory with list of parameters + $value = $params === [] ? $factory() : $factory(...$params); + + if (!\is_object($value) && !\is_scalar($value) && $value !== null) { + throw new \BadMethodCallException('Container variable $' . $name . ' expected type object|scalar|null from factory, but got ' . \gettype($value)); + } + + $this->container[$name] = $value; + } elseif (\is_array($this->container) && \array_key_exists($name, $this->container)) { + $value = $this->container[$name]; + } elseif (isset($_ENV[$name])) { + assert(\is_string($_ENV[$name])); + $value = $_ENV[$name]; + } elseif (isset($_SERVER[$name])) { + assert(\is_string($_SERVER[$name])); + $value = $_SERVER[$name]; + } else { + $value = \getenv($name); + assert($this->useProcessEnv && $value !== false); + } + + assert(\is_object($value) || \is_scalar($value) || $value === null); + + // allow null values if parameter is marked nullable or untyped or mixed + if ($nullable && $value === null) { + return null; + } + + // skip type checks and allow all values if expected type is undefined or mixed (PHP 8+) + if ($type === 'mixed') { + return $value; + } + + if ( + (\is_object($value) && !$value instanceof $type) || + (!\is_object($value) && !\in_array($type, ['string', 'int', 'float', 'bool'])) || + ($type === 'string' && !\is_string($value)) || ($type === 'int' && !\is_int($value)) || ($type === 'float' && !\is_float($value)) || ($type === 'bool' && !\is_bool($value)) + ) { + throw new \BadMethodCallException('Container variable $' . $name . ' expected type ' . $type . ', but got ' . (\is_object($value) ? \get_class($value) : \gettype($value))); + } + + return $value; + } + + /** @throws void */ + private static function parameterError(\ReflectionParameter $parameter): string + { + $name = $parameter->getDeclaringFunction()->getShortName(); + if (!$parameter->getDeclaringFunction()->isClosure() && ($class = $parameter->getDeclaringClass()) !== null) { + $name = explode("\0", $class->getName())[0] . '::' . $name; + } + + return 'Argument ' . ($parameter->getPosition() + 1) . ' ($' . $parameter->getName() . ') of ' . $name . '()'; + } +} diff --git a/vendor/clue/framework-x/src/ErrorHandler.php b/vendor/clue/framework-x/src/ErrorHandler.php new file mode 100644 index 0000000..e26d184 --- /dev/null +++ b/vendor/clue/framework-x/src/ErrorHandler.php @@ -0,0 +1,211 @@ +html = new HtmlHandler(); + } + + /** + * @return ResponseInterface|PromiseInterface|\Generator + * Returns a response, a Promise which eventually fulfills with a + * response or a Generator which eventually returns a response. This + * method never throws or resolves a rejected promise. If the next + * handler fails to return a valid response, it will be turned into a + * valid error response before returning. + * @throws void + */ + public function __invoke(ServerRequestInterface $request, callable $next) + { + try { + $response = $next($request); + } catch (\Throwable $e) { + return $this->errorInvalidException($e); + } + + if ($response instanceof ResponseInterface) { + return $response; + } elseif ($response instanceof PromiseInterface) { + return $response->then(function ($response) { + if ($response instanceof ResponseInterface) { + return $response; + } else { + return $this->errorInvalidResponse($response); + } + }, function ($e) { + // Promise rejected, always a `\Throwable` as of Promise v3 + assert($e instanceof \Throwable || !\method_exists(PromiseInterface::class, 'catch')); // @phpstan-ignore-line + + if ($e instanceof \Throwable) { + return $this->errorInvalidException($e); + } else { // @phpstan-ignore-line + // @phpstan-ignore-next-line + return $this->errorInvalidResponse(\React\Promise\reject($e)); // @codeCoverageIgnore + } + }); + } elseif ($response instanceof \Generator) { + return $this->coroutine($response); + } else { + return $this->errorInvalidResponse($response); + } + } + + private function coroutine(\Generator $generator): \Generator + { + do { + try { + if (!$generator->valid()) { + $response = $generator->getReturn(); + if ($response instanceof ResponseInterface) { + return $response; + } else { + return $this->errorInvalidResponse($response); + } + } + } catch (\Throwable $e) { + return $this->errorInvalidException($e); + } + + $promise = $generator->current(); + if (!$promise instanceof PromiseInterface) { + $gref = new \ReflectionGenerator($generator); + + return $this->errorInvalidCoroutine( + $promise, + $gref->getExecutingFile(), + $gref->getExecutingLine() + ); + } + + try { + $next = yield $promise; + } catch (\Throwable $e) { + try { + $generator->throw($e); + continue; + } catch (\Throwable $e) { + return $this->errorInvalidException($e); + } + } + + try { + $generator->send($next); + } catch (\Throwable $e) { + return $this->errorInvalidException($e); + } + } while (true); + } // @codeCoverageIgnore + + /** @internal */ + public function requestNotFound(): ResponseInterface + { + return $this->htmlResponse( + Response::STATUS_NOT_FOUND, + 'Page Not Found', + 'Please check the URL in the address bar and try again.' + ); + } + + /** + * @internal + * @param list $allowedMethods + */ + public function requestMethodNotAllowed(array $allowedMethods): ResponseInterface + { + $methods = \implode('/', \array_map(function (string $method) { return '' . $method . ''; }, $allowedMethods)); + + return $this->htmlResponse( + Response::STATUS_METHOD_NOT_ALLOWED, + 'Method Not Allowed', + 'Please check the URL in the address bar and try again with ' . $methods . ' request.' + )->withHeader('Allow', \implode(', ', $allowedMethods)); + } + + /** @internal */ + public function requestProxyUnsupported(): ResponseInterface + { + return $this->htmlResponse( + Response::STATUS_BAD_REQUEST, + 'Proxy Requests Not Allowed', + 'Please check your settings and retry.' + ); + } + + private function errorInvalidException(\Throwable $e): ResponseInterface + { + $where = ' in ' . $this->where($e->getFile(), $e->getLine()); + $message = '' . $this->html->escape($e->getMessage()) . ''; + + return $this->htmlResponse( + Response::STATUS_INTERNAL_SERVER_ERROR, + 'Internal Server Error', + 'The requested page failed to load, please try again later.', + 'Expected request handler to return ' . ResponseInterface::class . ' but got uncaught ' . \get_class($e) . ' with message ' . $message . $where . '.' + ); + } + + /** @param mixed $value */ + private function errorInvalidResponse($value): ResponseInterface + { + return $this->htmlResponse( + Response::STATUS_INTERNAL_SERVER_ERROR, + 'Internal Server Error', + 'The requested page failed to load, please try again later.', + 'Expected request handler to return ' . ResponseInterface::class . ' but got ' . $this->describeType($value) . '.' + ); + } + + /** @param mixed $value */ + private function errorInvalidCoroutine($value, string $file, int $line): ResponseInterface + { + $where = ' near or before '. $this->where($file, $line) . '.'; + + return $this->htmlResponse( + Response::STATUS_INTERNAL_SERVER_ERROR, + 'Internal Server Error', + 'The requested page failed to load, please try again later.', + 'Expected request handler to yield ' . PromiseInterface::class . ' but got ' . $this->describeType($value) . '' . $where + ); + } + + private function where(string $file, int $line): string + { + return '' . \basename($file) . ':' . $line . ''; + } + + private function htmlResponse(int $statusCode, string $title, string ...$info): ResponseInterface + { + return $this->html->statusResponse( + $statusCode, + 'Error ' . $statusCode . ': ' .$title, + $title, + \implode('', \array_map(function (string $info) { return "

$info

\n"; }, $info)) + ); + } + + /** @param mixed $value */ + private function describeType($value): string + { + if ($value === null) { + return 'null'; + } elseif (\is_scalar($value) && !\is_string($value)) { + return \var_export($value, true); + } + return \is_object($value) ? \get_class($value) : \gettype($value); + } +} diff --git a/vendor/clue/framework-x/src/FilesystemHandler.php b/vendor/clue/framework-x/src/FilesystemHandler.php new file mode 100644 index 0000000..567b53c --- /dev/null +++ b/vendor/clue/framework-x/src/FilesystemHandler.php @@ -0,0 +1,130 @@ + + */ + private $mimetypes = array( + 'atom' => 'application/atom+xml', + 'bz2' => 'application/x-bzip2', + 'css' => 'text/css', + 'gif' => 'image/gif', + 'gz' => 'application/gzip', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/x-icon', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'js' => 'text/javascript', + 'json' => 'application/json', + 'pdf' => 'application/pdf', + 'png' => 'image/png', + 'rss' => 'application/rss+xml', + 'svg' => 'image/svg+xml', + 'tar' => 'application/x-tar', + 'xml' => 'application/xml', + 'zip' => 'application/zip', + ); + + /** + * Assign default MIME type to send in `Content-Type` response header (same as nginx/Apache) + * + * @var string + * @see self::$mimetypes + */ + private $defaultMimetype = 'text/plain'; + + /** @var ErrorHandler */ + private $errorHandler; + + /** @var HtmlHandler */ + private $html; + + public function __construct(string $root) + { + $this->root = $root; + $this->errorHandler = new ErrorHandler(); + $this->html = new HtmlHandler(); + } + + public function __invoke(ServerRequestInterface $request): ResponseInterface + { + $local = $request->getAttribute('path', ''); + assert(\is_string($local)); + $path = \rtrim($this->root . '/' . $local, '/'); + + // local path should not contain "./", "../", "//" or null bytes or start with slash + $valid = !\preg_match('#(?:^|/)\.\.?(?:$|/)|^/|//|\x00#', $local); + + \clearstatcache(); + if ($valid && \is_dir($path)) { + if ($local !== '' && \substr($local, -1) !== '/') { + return (new RedirectHandler(\basename($path) . '/'))(); + } + + $response = '' . $this->html->escape($local === '' ? '/' : $local) . '' . "\n
    \n"; + + if ($local !== '') { + $response .= '
  • ../
  • ' . "\n"; + } + + $files = \scandir($path); + // @phpstan-ignore-next-line TODO handle error if directory can not be accessed + foreach ($files as $file) { + if ($file === '.' || $file === '..') { + continue; + } + + $dir = \is_dir($path . '/' . $file) ? '/' : ''; + $response .= '
  • ' . $this->html->escape($file) . $dir . '
  • ' . "\n"; + } + $response .= '
' . "\n"; + + return Response::html( + $response + ); + } elseif ($valid && \is_file($path)) { + if ($local !== '' && \substr($local, -1) === '/') { + return (new RedirectHandler('../' . \basename($path)))(); + } + + // Assign MIME type based on file extension (same as nginx/Apache) or fall back to given default otherwise. + // Browsers are pretty good at figuring out the correct type if no charset attribute is given. + $ext = \strtolower(\substr($path, \strrpos($path, '.') + 1)); + $headers = [ + 'Content-Type' => $this->mimetypes[$ext] ?? $this->defaultMimetype + ]; + + $stat = @\stat($path); + if ($stat !== false) { + $headers['Last-Modified'] = \gmdate('D, d M Y H:i:s', $stat['mtime']) . ' GMT'; + + if ($request->getHeaderLine('If-Modified-Since') === $headers['Last-Modified']) { + return new Response(Response::STATUS_NOT_MODIFIED); + } + } + + return new Response( + Response::STATUS_OK, + $headers, + \file_get_contents($path) // @phpstan-ignore-line TODO handle error if file can not be accessed + ); + } else { + return $this->errorHandler->requestNotFound(); + } + } +} diff --git a/vendor/clue/framework-x/src/Io/FiberHandler.php b/vendor/clue/framework-x/src/Io/FiberHandler.php new file mode 100644 index 0000000..07dc713 --- /dev/null +++ b/vendor/clue/framework-x/src/Io/FiberHandler.php @@ -0,0 +1,67 @@ +|\Generator + * Returns a `ResponseInterface` from the next request handler in the + * chain. If the next request handler returns immediately, this method + * will return immediately. If the next request handler suspends the + * fiber (see `await()`), this method will return a `PromiseInterface` + * that is fulfilled with a `ResponseInterface` when the fiber is + * terminated successfully. If the next request handler returns a + * promise, this method will return a promise that follows its + * resolution. If the next request handler returns a Generator-based + * coroutine, this method returns a `Generator`. This method never + * throws or resolves a rejected promise. If the handler fails, it will + * be turned into a valid error response before returning. + * @throws void + */ + public function __invoke(ServerRequestInterface $request, callable $next) + { + $deferred = null; + $fiber = new \Fiber(function () use ($request, $next, &$deferred) { + $response = $next($request); + assert($response instanceof ResponseInterface || $response instanceof PromiseInterface || $response instanceof \Generator); + + // if the next request handler returns immediately, the fiber can terminate immediately without using a Deferred + // if the next request handler suspends the fiber, we only reach this point after resuming the fiber, so the code below will have assigned a Deferred + /** @var ?Deferred $deferred */ + if ($deferred !== null) { + assert($response instanceof ResponseInterface); + $deferred->resolve($response); + } + + return $response; + }); + + /** @throws void because the next handler will always be an `ErrorHandler` */ + $fiber->start(); + if ($fiber->isTerminated()) { + /** @throws void because fiber is known to have terminated successfully */ + /** @var ResponseInterface|PromiseInterface|\Generator */ + return $fiber->getReturn(); + } + + $deferred = new Deferred(); + return $deferred->promise(); + } +} diff --git a/vendor/clue/framework-x/src/Io/HtmlHandler.php b/vendor/clue/framework-x/src/Io/HtmlHandler.php new file mode 100644 index 0000000..0e87e65 --- /dev/null +++ b/vendor/clue/framework-x/src/Io/HtmlHandler.php @@ -0,0 +1,65 @@ + + + +$title + + + +
+

$statusCode

+$subtitle +$info
+ + + +HTML; + + return new Response( + $statusCode, + [ + 'Content-Type' => 'text/html; charset=utf-8', + 'Content-Security-Policy' => "style-src 'nonce-$nonce'; img-src 'self'; default-src 'none'" + ], + $html + ); + } + + public function escape(string $s): string + { + return (string) \preg_replace_callback( + '/[\x00-\x1F]+/', + function (array $match): string { + return '' . \addcslashes($match[0], "\x00..\xff") . ''; + }, + (string) \preg_replace( + '/(^| ) |(?: $)/', + '$1 ', + \htmlspecialchars($s, \ENT_NOQUOTES | \ENT_SUBSTITUTE | \ENT_DISALLOWED, 'utf-8') + ) + ); + } +} diff --git a/vendor/clue/framework-x/src/Io/LogStreamHandler.php b/vendor/clue/framework-x/src/Io/LogStreamHandler.php new file mode 100644 index 0000000..8c93349 --- /dev/null +++ b/vendor/clue/framework-x/src/Io/LogStreamHandler.php @@ -0,0 +1,95 @@ +isAbsolutePath($path))) { + throw new \InvalidArgumentException( + 'Unable to open log file "' . \addslashes($path) . '": Invalid path given' + ); + } + + $errstr = ''; + \set_error_handler(function (int $_, string $error) use (&$errstr): bool { + // Match errstr from PHP's warning message. + // fopen(/dev/not-a-valid-path): Failed to open stream: Permission denied + $errstr = \preg_replace('#.*: #', '', $error); + + return true; + }); + + $stream = \fopen($path, 'ae'); + + // try to fstat($stream) to see if this points to /dev/null (skip on Windows) + // @codeCoverageIgnoreStart + $stat = false; + if ($stream !== false && \DIRECTORY_SEPARATOR !== '\\') { + if (\strtolower($path) === 'php://output') { + // php://output doesn't support stat, so assume php://output will go to php://stdout + $stdout = \defined('STDOUT') ? \STDOUT : \fopen('php://stdout', 'w'); + if (\is_resource($stdout)) { + $stat = \fstat($stdout); + } else { + // STDOUT can not be opened => assume piping to /dev/null + $stream = null; + } + } else { + $stat = \fstat($stream); + } + + // close stream if it points to /dev/null + if ($stat !== false && $stat === \stat('/dev/null')) { + $stream = null; + } + } + // @codeCoverageIgnoreEnd + + \restore_error_handler(); + + if ($stream === false) { + throw new \RuntimeException( + 'Unable to open log file "' . $path . '": ' . $errstr + ); + } + + $this->stream = $stream; + } + + public function isDevNull(): bool + { + return $this->stream === null; + } + + public function log(string $message): void + { + // nothing to do if we're writing to /dev/null + if ($this->stream === null) { + return; // @codeCoverageIgnore + } + + $time = \microtime(true); + $prefix = \date('Y-m-d H:i:s', (int) $time) . \sprintf('.%03d ', (int) (($time - (int) $time) * 1e3)); + + $ret = \fwrite($this->stream, $prefix . $message . \PHP_EOL); + assert(\is_int($ret)); + } + + private function isAbsolutePath(string $path): bool + { + return \DIRECTORY_SEPARATOR !== '\\' ? \substr($path, 0, 1) === '/' : (bool) \preg_match('#^[A-Z]:[/\\\\]#i', $path); + } +} diff --git a/vendor/clue/framework-x/src/Io/MiddlewareHandler.php b/vendor/clue/framework-x/src/Io/MiddlewareHandler.php new file mode 100644 index 0000000..dd62f01 --- /dev/null +++ b/vendor/clue/framework-x/src/Io/MiddlewareHandler.php @@ -0,0 +1,40 @@ + $handlers */ + private $handlers; + + /** @param list $handlers */ + public function __construct(array $handlers) + { + assert(count($handlers) >= 2); + + $this->handlers = $handlers; + } + + /** @return mixed */ + public function __invoke(ServerRequestInterface $request) + { + return $this->call($request, 0); + } + + /** @return mixed */ + private function call(ServerRequestInterface $request, int $position) + { + if (!isset($this->handlers[$position + 2])) { + return $this->handlers[$position]($request, $this->handlers[$position + 1]); + } + + return $this->handlers[$position]($request, function (ServerRequestInterface $request) use ($position) { + return $this->call($request, $position + 1); + }); + } +} diff --git a/vendor/clue/framework-x/src/Io/ReactiveHandler.php b/vendor/clue/framework-x/src/Io/ReactiveHandler.php new file mode 100644 index 0000000..579eb8a --- /dev/null +++ b/vendor/clue/framework-x/src/Io/ReactiveHandler.php @@ -0,0 +1,91 @@ +logger = new LogStreamHandler('php://output'); + $this->listenAddress = $listenAddress ?? '127.0.0.1:8080'; + } + + public function run(callable $handler): void + { + $socket = new SocketServer($this->listenAddress); + + // create HTTP server, automatically start new fiber for each request on PHP 8.1+ + $http = new HttpServer(...(\PHP_VERSION_ID >= 80100 ? [new FiberHandler(), $handler] : [$handler])); + $http->listen($socket); + + $logger = $this->logger; + $logger->log('Listening on ' . \str_replace('tcp:', 'http:', (string) $socket->getAddress())); + + $http->on('error', static function (\Exception $e) use ($logger): void { + $logger->log('HTTP error: ' . $e->getMessage()); + }); + + // @codeCoverageIgnoreStart + try { + Loop::addSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 = static function () use ($socket, $logger): void { + if (\PHP_VERSION_ID >= 70200 && \stream_isatty(\STDIN)) { + echo "\r"; + } + $logger->log('Received SIGINT, stopping loop'); + + $socket->close(); + Loop::stop(); + }); + Loop::addSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 = static function () use ($socket, $logger): void { + $logger->log('Received SIGTERM, stopping loop'); + + $socket->close(); + Loop::stop(); + }); + } catch (\BadMethodCallException $e) { + $logger->log('Notice: No signal handler support, installing ext-ev or ext-pcntl recommended for production use.'); + } + // @codeCoverageIgnoreEnd + + do { + Loop::run(); + + if ($socket->getAddress() !== null) { + // Fiber compatibility mode for PHP < 8.1: Restart loop as long as socket is available + $logger->log('Warning: Loop restarted. Upgrade to react/async v4 recommended for production use.'); + } else { + break; + } + } while (true); + + // remove signal handlers when loop stops (if registered) + Loop::removeSignal(\defined('SIGINT') ? \SIGINT : 2, $f1 ?? 'printf'); + Loop::removeSignal(\defined('SIGTERM') ? \SIGTERM : 15, $f2 ?? 'printf'); + } +} diff --git a/vendor/clue/framework-x/src/Io/RedirectHandler.php b/vendor/clue/framework-x/src/Io/RedirectHandler.php new file mode 100644 index 0000000..3924ccb --- /dev/null +++ b/vendor/clue/framework-x/src/Io/RedirectHandler.php @@ -0,0 +1,48 @@ += 400) { + throw new \InvalidArgumentException('Invalid redirect status code given'); + } + + $this->target = $target; + $this->code = $redirectStatusCode; + $this->reason = \ucwords((new Response($redirectStatusCode))->getReasonPhrase()) ?: 'Redirect'; + $this->html = new HtmlHandler(); + } + + public function __invoke(): ResponseInterface + { + $url = $this->html->escape($this->target); + + return $this->html->statusResponse( + $this->code, + 'Redirecting to ' . $url, + $this->reason, + "

Redirecting to $url...

\n" + )->withHeader('Location', $this->target); + } +} diff --git a/vendor/clue/framework-x/src/Io/RouteHandler.php b/vendor/clue/framework-x/src/Io/RouteHandler.php new file mode 100644 index 0000000..2b0cf1c --- /dev/null +++ b/vendor/clue/framework-x/src/Io/RouteHandler.php @@ -0,0 +1,116 @@ +routeCollector = new RouteCollector(new RouteParser(), new RouteGenerator()); + $this->errorHandler = new ErrorHandler(); + $this->container = $container ?? new Container(); + } + + /** + * @param string[] $methods + * @param string $route + * @param callable|class-string $handler + * @param callable|class-string ...$handlers + */ + public function map(array $methods, string $route, $handler, ...$handlers): void + { + if ($handlers) { + \array_unshift($handlers, $handler); + \end($handlers); + } else { + $handlers = [$handler]; + } + + $last = key($handlers); + $container = $this->container; + foreach ($handlers as $i => $handler) { + if ($handler instanceof Container && $i !== $last) { + $container = $handler; + unset($handlers[$i]); + } elseif ($handler instanceof AccessLogHandler || $handler === AccessLogHandler::class) { + throw new \TypeError('AccessLogHandler may currently only be passed as a global middleware'); + } elseif (!\is_callable($handler)) { + $handlers[$i] = $container->callable($handler); + } + } + + /** @var non-empty-array $handlers */ + $handler = \count($handlers) > 1 ? new MiddlewareHandler(array_values($handlers)) : \reset($handlers); + $this->routeDispatcher = null; + $this->routeCollector->addRoute($methods, $route, $handler); + } + + /** + * @return ResponseInterface|PromiseInterface|\Generator + */ + public function __invoke(ServerRequestInterface $request) + { + $target = $request->getRequestTarget(); + if ($target[0] !== '/' && $target !== '*') { + return $this->errorHandler->requestProxyUnsupported(); + } elseif ($target !== '*') { + $target = $request->getUri()->getPath(); + } + + if ($this->routeDispatcher === null) { + $this->routeDispatcher = new RouteDispatcher($this->routeCollector->getData()); + } + + $routeInfo = $this->routeDispatcher->dispatch($request->getMethod(), $target); + assert(\is_array($routeInfo) && isset($routeInfo[0])); + + // happy path: matching route found, assign route attributes and invoke request handler + if ($routeInfo[0] === \FastRoute\Dispatcher::FOUND) { + $handler = $routeInfo[1]; + $vars = $routeInfo[2]; + + foreach ($vars as $key => $value) { + $request = $request->withAttribute($key, rawurldecode($value)); + } + + return $handler($request); + } + + // no matching route found: report error `404 Not Found` + if ($routeInfo[0] === \FastRoute\Dispatcher::NOT_FOUND) { + return $this->errorHandler->requestNotFound(); + } + + // unexpected request method for route: report error `405 Method Not Allowed` + assert($routeInfo[0] === \FastRoute\Dispatcher::METHOD_NOT_ALLOWED); + assert(\is_array($routeInfo[1]) && \count($routeInfo[1]) > 0); + + return $this->errorHandler->requestMethodNotAllowed($routeInfo[1]); + } +} diff --git a/vendor/clue/framework-x/src/Io/SapiHandler.php b/vendor/clue/framework-x/src/Io/SapiHandler.php new file mode 100644 index 0000000..d3faf27 --- /dev/null +++ b/vendor/clue/framework-x/src/Io/SapiHandler.php @@ -0,0 +1,164 @@ +requestFromGlobals(); + + $response = $handler($request); + + if ($response instanceof ResponseInterface) { + $this->sendResponse($response); + } elseif ($response instanceof PromiseInterface) { + /** @var PromiseInterface $response */ + $response->then(function (ResponseInterface $response): void { + $this->sendResponse($response); + }); + } + + Loop::run(); + } + + public function requestFromGlobals(): ServerRequestInterface + { + $host = null; + $headers = array(); + // @codeCoverageIgnoreStart + if (\function_exists('getallheaders')) { + $headers = \getallheaders(); + $host = \array_change_key_case($headers, \CASE_LOWER)['host'] ?? null; + } else { + foreach ($_SERVER as $key => $value) { + if (\strpos($key, 'HTTP_') === 0) { + $key = str_replace(' ', '-', \ucwords(\strtolower(\str_replace('_', ' ', \substr($key, 5))))); + $headers[$key] = $value; + + if ($host === null && $key === 'Host') { + $host = $value; + } + } + } + } + // @codeCoverageIgnoreEnd + + $target = ($_SERVER['REQUEST_URI'] ?? '/'); + $url = $target; + if (($target[0] ?? '/') === '/' || $target === '*') { + $url = (($_SERVER['HTTPS'] ?? null) === 'on' ? 'https://' : 'http://') . ($host ?? 'localhost') . ($target === '*' ? '' : $target); + } + + $body = file_get_contents('php://input'); + assert(\is_string($body)); + + $request = new ServerRequest( + $_SERVER['REQUEST_METHOD'] ?? 'GET', + $url, + $headers, + $body, + substr($_SERVER['SERVER_PROTOCOL'] ?? 'http/1.1', 5), + $_SERVER + ); + if ($host === null) { + $request = $request->withoutHeader('Host'); + } + if (isset($target[0]) && $target[0] !== '/') { + $request = $request->withRequestTarget($target); + } + $request = $request->withParsedBody($_POST); + + // Content-Length / Content-Type are special <3 + if ($request->getHeaderLine('Content-Length') === '') { + $request = $request->withoutHeader('Content-Length'); + } + if ($request->getHeaderLine('Content-Type') === '' && !isset($_SERVER['HTTP_CONTENT_TYPE'])) { + $request = $request->withoutHeader('Content-Type'); + } + + return $request; + } + + /** + * @param ResponseInterface $response + */ + public function sendResponse(ResponseInterface $response): void + { + $status = $response->getStatusCode(); + $body = $response->getBody(); + + if ($status === Response::STATUS_NO_CONTENT) { + // `204 No Content` MUST NOT include "Content-Length" response header + $response = $response->withoutHeader('Content-Length'); + } elseif (!$response->hasHeader('Content-Length') && $body->getSize() !== null && ($status !== Response::STATUS_NOT_MODIFIED || $body->getSize() !== 0)) { + // automatically assign "Content-Length" response header if known and not already present + $response = $response->withHeader('Content-Length', (string) $body->getSize()); + } + + // remove default "Content-Type" header set by PHP (default_mimetype) + if (!$response->hasHeader('Content-Type')) { + header('Content-Type:'); + header_remove('Content-Type'); + } + + // send all headers without applying default "; charset=utf-8" set by PHP (default_charset) + $old = ini_get('default_charset'); + assert(\is_string($old)); + ini_set('default_charset', ''); + foreach ($response->getHeaders() as $name => $values) { + foreach ($values as $value) { + header($name . ': ' . $value, false); + } + } + ini_set('default_charset', $old); + + header($_SERVER['SERVER_PROTOCOL'] . ' ' . $status . ' ' . $response->getReasonPhrase()); + + if (($_SERVER['REQUEST_METHOD'] ?? '') === 'HEAD' || $status === Response::STATUS_NO_CONTENT || $status === Response::STATUS_NOT_MODIFIED) { + $body->close(); + return; + } + + if ($body instanceof ReadableStreamInterface) { + // try to disable nginx buffering (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering) + if (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'nginx') === 0) { + header('X-Accel-Buffering: no'); + } + + // clear output buffer to show streaming output (default in cli-server) + if (\PHP_SAPI === 'cli-server') { + \ob_end_flush(); // @codeCoverageIgnore + } + + // flush data whenever stream reports one data chunk + $body->on('data', function ($chunk) { + echo $chunk; + flush(); + }); + } else { + echo $body; + } + } +} diff --git a/vendor/fig/http-message-util/.gitignore b/vendor/fig/http-message-util/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/vendor/fig/http-message-util/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/vendor/fig/http-message-util/CHANGELOG.md b/vendor/fig/http-message-util/CHANGELOG.md new file mode 100644 index 0000000..1a02e54 --- /dev/null +++ b/vendor/fig/http-message-util/CHANGELOG.md @@ -0,0 +1,147 @@ +# Changelog + +All notable changes to this project will be documented in this file, in reverse chronological order by release. + +## 1.1.5 - 2020-11-24 + +### Added + +- [#19](https://github.com/php-fig/http-message-util/pull/19) adds support for PHP 8. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.1.4 - 2020-02-05 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- [#15](https://github.com/php-fig/http-message-util/pull/15) removes the dependency on psr/http-message, as it is not technically necessary for usage of this package. + +### Fixed + +- Nothing. + +## 1.1.3 - 2018-11-19 + +### Added + +- [#10](https://github.com/php-fig/http-message-util/pull/10) adds the constants `StatusCodeInterface::STATUS_EARLY_HINTS` (103) and + `StatusCodeInterface::STATUS_TOO_EARLY` (425). + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.1.2 - 2017-02-09 + +### Added + +- [#4](https://github.com/php-fig/http-message-util/pull/4) adds the constant + `StatusCodeInterface::STATUS_MISDIRECTED_REQUEST` (421). + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.1.1 - 2017-02-06 + +### Added + +- [#3](https://github.com/php-fig/http-message-util/pull/3) adds the constant + `StatusCodeInterface::STATUS_IM_A_TEAPOT` (418). + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.1.0 - 2016-09-19 + +### Added + +- [#1](https://github.com/php-fig/http-message-util/pull/1) adds + `Fig\Http\Message\StatusCodeInterface`, with constants named after common + status reason phrases, with values indicating the status codes themselves. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.0.0 - 2017-08-05 + +### Added + +- Adds `Fig\Http\Message\RequestMethodInterface`, with constants covering the + most common HTTP request methods as specified by the IETF. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. diff --git a/vendor/fig/http-message-util/LICENSE b/vendor/fig/http-message-util/LICENSE new file mode 100644 index 0000000..e2fa347 --- /dev/null +++ b/vendor/fig/http-message-util/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 PHP Framework Interoperability Group + +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/fig/http-message-util/README.md b/vendor/fig/http-message-util/README.md new file mode 100644 index 0000000..ea5b5aa --- /dev/null +++ b/vendor/fig/http-message-util/README.md @@ -0,0 +1,17 @@ +# PSR Http Message Util + +This repository holds utility classes and constants to facilitate common +operations of [PSR-7](https://www.php-fig.org/psr/psr-7/); the primary purpose is +to provide constants for referring to request methods, response status codes and +messages, and potentially common headers. + +Implementation of PSR-7 interfaces is **not** within the scope of this package. + +## Installation + +Install by adding the package as a [Composer](https://getcomposer.org) +requirement: + +```bash +$ composer require fig/http-message-util +``` diff --git a/vendor/fig/http-message-util/composer.json b/vendor/fig/http-message-util/composer.json new file mode 100644 index 0000000..8645893 --- /dev/null +++ b/vendor/fig/http-message-util/composer.json @@ -0,0 +1,28 @@ +{ + "name": "fig/http-message-util", + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": ["psr", "psr-7", "http", "http-message", "request", "response"], + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/vendor/fig/http-message-util/src/RequestMethodInterface.php b/vendor/fig/http-message-util/src/RequestMethodInterface.php new file mode 100644 index 0000000..97d9a93 --- /dev/null +++ b/vendor/fig/http-message-util/src/RequestMethodInterface.php @@ -0,0 +1,34 @@ + + * class RequestFactory implements RequestMethodInterface + * { + * public static function factory( + * $uri = '/', + * $method = self::METHOD_GET, + * $data = [] + * ) { + * } + * } + * + */ +interface RequestMethodInterface +{ + const METHOD_HEAD = 'HEAD'; + const METHOD_GET = 'GET'; + const METHOD_POST = 'POST'; + const METHOD_PUT = 'PUT'; + const METHOD_PATCH = 'PATCH'; + const METHOD_DELETE = 'DELETE'; + const METHOD_PURGE = 'PURGE'; + const METHOD_OPTIONS = 'OPTIONS'; + const METHOD_TRACE = 'TRACE'; + const METHOD_CONNECT = 'CONNECT'; +} diff --git a/vendor/fig/http-message-util/src/StatusCodeInterface.php b/vendor/fig/http-message-util/src/StatusCodeInterface.php new file mode 100644 index 0000000..99b7e78 --- /dev/null +++ b/vendor/fig/http-message-util/src/StatusCodeInterface.php @@ -0,0 +1,107 @@ + + * class ResponseFactory implements StatusCodeInterface + * { + * public function createResponse($code = self::STATUS_OK) + * { + * } + * } + * + */ +interface StatusCodeInterface +{ + // Informational 1xx + const STATUS_CONTINUE = 100; + const STATUS_SWITCHING_PROTOCOLS = 101; + const STATUS_PROCESSING = 102; + const STATUS_EARLY_HINTS = 103; + // Successful 2xx + const STATUS_OK = 200; + const STATUS_CREATED = 201; + const STATUS_ACCEPTED = 202; + const STATUS_NON_AUTHORITATIVE_INFORMATION = 203; + const STATUS_NO_CONTENT = 204; + const STATUS_RESET_CONTENT = 205; + const STATUS_PARTIAL_CONTENT = 206; + const STATUS_MULTI_STATUS = 207; + const STATUS_ALREADY_REPORTED = 208; + const STATUS_IM_USED = 226; + // Redirection 3xx + const STATUS_MULTIPLE_CHOICES = 300; + const STATUS_MOVED_PERMANENTLY = 301; + const STATUS_FOUND = 302; + const STATUS_SEE_OTHER = 303; + const STATUS_NOT_MODIFIED = 304; + const STATUS_USE_PROXY = 305; + const STATUS_RESERVED = 306; + const STATUS_TEMPORARY_REDIRECT = 307; + const STATUS_PERMANENT_REDIRECT = 308; + // Client Errors 4xx + const STATUS_BAD_REQUEST = 400; + const STATUS_UNAUTHORIZED = 401; + const STATUS_PAYMENT_REQUIRED = 402; + const STATUS_FORBIDDEN = 403; + const STATUS_NOT_FOUND = 404; + const STATUS_METHOD_NOT_ALLOWED = 405; + const STATUS_NOT_ACCEPTABLE = 406; + const STATUS_PROXY_AUTHENTICATION_REQUIRED = 407; + const STATUS_REQUEST_TIMEOUT = 408; + const STATUS_CONFLICT = 409; + const STATUS_GONE = 410; + const STATUS_LENGTH_REQUIRED = 411; + const STATUS_PRECONDITION_FAILED = 412; + const STATUS_PAYLOAD_TOO_LARGE = 413; + const STATUS_URI_TOO_LONG = 414; + const STATUS_UNSUPPORTED_MEDIA_TYPE = 415; + const STATUS_RANGE_NOT_SATISFIABLE = 416; + const STATUS_EXPECTATION_FAILED = 417; + const STATUS_IM_A_TEAPOT = 418; + const STATUS_MISDIRECTED_REQUEST = 421; + const STATUS_UNPROCESSABLE_ENTITY = 422; + const STATUS_LOCKED = 423; + const STATUS_FAILED_DEPENDENCY = 424; + const STATUS_TOO_EARLY = 425; + const STATUS_UPGRADE_REQUIRED = 426; + const STATUS_PRECONDITION_REQUIRED = 428; + const STATUS_TOO_MANY_REQUESTS = 429; + const STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; + const STATUS_UNAVAILABLE_FOR_LEGAL_REASONS = 451; + // Server Errors 5xx + const STATUS_INTERNAL_SERVER_ERROR = 500; + const STATUS_NOT_IMPLEMENTED = 501; + const STATUS_BAD_GATEWAY = 502; + const STATUS_SERVICE_UNAVAILABLE = 503; + const STATUS_GATEWAY_TIMEOUT = 504; + const STATUS_VERSION_NOT_SUPPORTED = 505; + const STATUS_VARIANT_ALSO_NEGOTIATES = 506; + const STATUS_INSUFFICIENT_STORAGE = 507; + const STATUS_LOOP_DETECTED = 508; + const STATUS_NOT_EXTENDED = 510; + const STATUS_NETWORK_AUTHENTICATION_REQUIRED = 511; +} diff --git a/vendor/nikic/fast-route/.gitignore b/vendor/nikic/fast-route/.gitignore new file mode 100644 index 0000000..e378a07 --- /dev/null +++ b/vendor/nikic/fast-route/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +.idea/ + +# ignore lock file since we have no extra dependencies +composer.lock diff --git a/vendor/nikic/fast-route/.hhconfig b/vendor/nikic/fast-route/.hhconfig new file mode 100644 index 0000000..0c2153c --- /dev/null +++ b/vendor/nikic/fast-route/.hhconfig @@ -0,0 +1 @@ +assume_php=false diff --git a/vendor/nikic/fast-route/.travis.yml b/vendor/nikic/fast-route/.travis.yml new file mode 100644 index 0000000..10f8381 --- /dev/null +++ b/vendor/nikic/fast-route/.travis.yml @@ -0,0 +1,20 @@ +sudo: false +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - hhvm + +script: + - ./vendor/bin/phpunit + +before_install: + - travis_retry composer self-update + +install: + - composer install diff --git a/vendor/nikic/fast-route/FastRoute.hhi b/vendor/nikic/fast-route/FastRoute.hhi new file mode 100644 index 0000000..8d50738 --- /dev/null +++ b/vendor/nikic/fast-route/FastRoute.hhi @@ -0,0 +1,126 @@ +; + } + + class RouteCollector { + public function __construct(RouteParser $routeParser, DataGenerator $dataGenerator); + public function addRoute(mixed $httpMethod, string $route, mixed $handler): void; + public function getData(): array; + } + + class Route { + public function __construct(string $httpMethod, mixed $handler, string $regex, array $variables); + public function matches(string $str): bool; + } + + interface DataGenerator { + public function addRoute(string $httpMethod, array $routeData, mixed $handler); + public function getData(): array; + } + + interface Dispatcher { + const int NOT_FOUND = 0; + const int FOUND = 1; + const int METHOD_NOT_ALLOWED = 2; + public function dispatch(string $httpMethod, string $uri): array; + } + + function simpleDispatcher( + (function(RouteCollector): void) $routeDefinitionCallback, + shape( + ?'routeParser' => classname, + ?'dataGenerator' => classname, + ?'dispatcher' => classname, + ?'routeCollector' => classname, + ) $options = shape()): Dispatcher; + + function cachedDispatcher( + (function(RouteCollector): void) $routeDefinitionCallback, + shape( + ?'routeParser' => classname, + ?'dataGenerator' => classname, + ?'dispatcher' => classname, + ?'routeCollector' => classname, + ?'cacheDisabled' => bool, + ?'cacheFile' => string, + ) $options = shape()): Dispatcher; +} + +namespace FastRoute\DataGenerator { + abstract class RegexBasedAbstract implements \FastRoute\DataGenerator { + protected abstract function getApproxChunkSize(); + protected abstract function processChunk($regexToRoutesMap); + + public function addRoute(string $httpMethod, array $routeData, mixed $handler): void; + public function getData(): array; + } + + class CharCountBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } + + class GroupCountBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } + + class GroupPosBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } + + class MarkBased extends RegexBasedAbstract { + protected function getApproxChunkSize(): int; + protected function processChunk(array $regexToRoutesMap): array; + } +} + +namespace FastRoute\Dispatcher { + abstract class RegexBasedAbstract implements \FastRoute\Dispatcher { + protected abstract function dispatchVariableRoute(array $routeData, string $uri): array; + + public function dispatch(string $httpMethod, string $uri): array; + } + + class GroupPosBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } + + class GroupCountBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } + + class CharCountBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } + + class MarkBased extends RegexBasedAbstract { + public function __construct(array $data); + protected function dispatchVariableRoute(array $routeData, string $uri): array; + } +} + +namespace FastRoute\RouteParser { + class Std implements \FastRoute\RouteParser { + const string VARIABLE_REGEX = <<<'REGEX' +\{ + \s* ([a-zA-Z][a-zA-Z0-9_]*) \s* + (?: + : \s* ([^{}]*(?:\{(?-1)\}[^{}]*)*) + )? +\} +REGEX; + const string DEFAULT_DISPATCH_REGEX = '[^/]+'; + public function parse(string $route): array; + } +} diff --git a/vendor/nikic/fast-route/LICENSE b/vendor/nikic/fast-route/LICENSE new file mode 100644 index 0000000..478e764 --- /dev/null +++ b/vendor/nikic/fast-route/LICENSE @@ -0,0 +1,31 @@ +Copyright (c) 2013 by Nikita Popov. + +Some rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/nikic/fast-route/README.md b/vendor/nikic/fast-route/README.md new file mode 100644 index 0000000..91bd466 --- /dev/null +++ b/vendor/nikic/fast-route/README.md @@ -0,0 +1,313 @@ +FastRoute - Fast request router for PHP +======================================= + +This library provides a fast implementation of a regular expression based router. [Blog post explaining how the +implementation works and why it is fast.][blog_post] + +Install +------- + +To install with composer: + +```sh +composer require nikic/fast-route +``` + +Requires PHP 5.4 or newer. + +Usage +----- + +Here's a basic usage example: + +```php +addRoute('GET', '/users', 'get_all_users_handler'); + // {id} must be a number (\d+) + $r->addRoute('GET', '/user/{id:\d+}', 'get_user_handler'); + // The /{title} suffix is optional + $r->addRoute('GET', '/articles/{id:\d+}[/{title}]', 'get_article_handler'); +}); + +// Fetch method and URI from somewhere +$httpMethod = $_SERVER['REQUEST_METHOD']; +$uri = $_SERVER['REQUEST_URI']; + +// Strip query string (?foo=bar) and decode URI +if (false !== $pos = strpos($uri, '?')) { + $uri = substr($uri, 0, $pos); +} +$uri = rawurldecode($uri); + +$routeInfo = $dispatcher->dispatch($httpMethod, $uri); +switch ($routeInfo[0]) { + case FastRoute\Dispatcher::NOT_FOUND: + // ... 404 Not Found + break; + case FastRoute\Dispatcher::METHOD_NOT_ALLOWED: + $allowedMethods = $routeInfo[1]; + // ... 405 Method Not Allowed + break; + case FastRoute\Dispatcher::FOUND: + $handler = $routeInfo[1]; + $vars = $routeInfo[2]; + // ... call $handler with $vars + break; +} +``` + +### Defining routes + +The routes are defined by calling the `FastRoute\simpleDispatcher()` function, which accepts +a callable taking a `FastRoute\RouteCollector` instance. The routes are added by calling +`addRoute()` on the collector instance: + +```php +$r->addRoute($method, $routePattern, $handler); +``` + +The `$method` is an uppercase HTTP method string for which a certain route should match. It +is possible to specify multiple valid methods using an array: + +```php +// These two calls +$r->addRoute('GET', '/test', 'handler'); +$r->addRoute('POST', '/test', 'handler'); +// Are equivalent to this one call +$r->addRoute(['GET', 'POST'], '/test', 'handler'); +``` + +By default the `$routePattern` uses a syntax where `{foo}` specifies a placeholder with name `foo` +and matching the regex `[^/]+`. To adjust the pattern the placeholder matches, you can specify +a custom pattern by writing `{bar:[0-9]+}`. Some examples: + +```php +// Matches /user/42, but not /user/xyz +$r->addRoute('GET', '/user/{id:\d+}', 'handler'); + +// Matches /user/foobar, but not /user/foo/bar +$r->addRoute('GET', '/user/{name}', 'handler'); + +// Matches /user/foo/bar as well +$r->addRoute('GET', '/user/{name:.+}', 'handler'); +``` + +Custom patterns for route placeholders cannot use capturing groups. For example `{lang:(en|de)}` +is not a valid placeholder, because `()` is a capturing group. Instead you can use either +`{lang:en|de}` or `{lang:(?:en|de)}`. + +Furthermore parts of the route enclosed in `[...]` are considered optional, so that `/foo[bar]` +will match both `/foo` and `/foobar`. Optional parts are only supported in a trailing position, +not in the middle of a route. + +```php +// This route +$r->addRoute('GET', '/user/{id:\d+}[/{name}]', 'handler'); +// Is equivalent to these two routes +$r->addRoute('GET', '/user/{id:\d+}', 'handler'); +$r->addRoute('GET', '/user/{id:\d+}/{name}', 'handler'); + +// Multiple nested optional parts are possible as well +$r->addRoute('GET', '/user[/{id:\d+}[/{name}]]', 'handler'); + +// This route is NOT valid, because optional parts can only occur at the end +$r->addRoute('GET', '/user[/{id:\d+}]/{name}', 'handler'); +``` + +The `$handler` parameter does not necessarily have to be a callback, it could also be a controller +class name or any other kind of data you wish to associate with the route. FastRoute only tells you +which handler corresponds to your URI, how you interpret it is up to you. + +#### Shorcut methods for common request methods + +For the `GET`, `POST`, `PUT`, `PATCH`, `DELETE` and `HEAD` request methods shortcut methods are available. For example: + +```php +$r->get('/get-route', 'get_handler'); +$r->post('/post-route', 'post_handler'); +``` + +Is equivalent to: + +```php +$r->addRoute('GET', '/get-route', 'get_handler'); +$r->addRoute('POST', '/post-route', 'post_handler'); +``` + +#### Route Groups + +Additionally, you can specify routes inside of a group. All routes defined inside a group will have a common prefix. + +For example, defining your routes as: + +```php +$r->addGroup('/admin', function (RouteCollector $r) { + $r->addRoute('GET', '/do-something', 'handler'); + $r->addRoute('GET', '/do-another-thing', 'handler'); + $r->addRoute('GET', '/do-something-else', 'handler'); +}); +``` + +Will have the same result as: + + ```php +$r->addRoute('GET', '/admin/do-something', 'handler'); +$r->addRoute('GET', '/admin/do-another-thing', 'handler'); +$r->addRoute('GET', '/admin/do-something-else', 'handler'); + ``` + +Nested groups are also supported, in which case the prefixes of all the nested groups are combined. + +### Caching + +The reason `simpleDispatcher` accepts a callback for defining the routes is to allow seamless +caching. By using `cachedDispatcher` instead of `simpleDispatcher` you can cache the generated +routing data and construct the dispatcher from the cached information: + +```php +addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); +}, [ + 'cacheFile' => __DIR__ . '/route.cache', /* required */ + 'cacheDisabled' => IS_DEBUG_ENABLED, /* optional, enabled by default */ +]); +``` + +The second parameter to the function is an options array, which can be used to specify the cache +file location, among other things. + +### Dispatching a URI + +A URI is dispatched by calling the `dispatch()` method of the created dispatcher. This method +accepts the HTTP method and a URI. Getting those two bits of information (and normalizing them +appropriately) is your job - this library is not bound to the PHP web SAPIs. + +The `dispatch()` method returns an array whose first element contains a status code. It is one +of `Dispatcher::NOT_FOUND`, `Dispatcher::METHOD_NOT_ALLOWED` and `Dispatcher::FOUND`. For the +method not allowed status the second array element contains a list of HTTP methods allowed for +the supplied URI. For example: + + [FastRoute\Dispatcher::METHOD_NOT_ALLOWED, ['GET', 'POST']] + +> **NOTE:** The HTTP specification requires that a `405 Method Not Allowed` response include the +`Allow:` header to detail available methods for the requested resource. Applications using FastRoute +should use the second array element to add this header when relaying a 405 response. + +For the found status the second array element is the handler that was associated with the route +and the third array element is a dictionary of placeholder names to their values. For example: + + /* Routing against GET /user/nikic/42 */ + + [FastRoute\Dispatcher::FOUND, 'handler0', ['name' => 'nikic', 'id' => '42']] + +### Overriding the route parser and dispatcher + +The routing process makes use of three components: A route parser, a data generator and a +dispatcher. The three components adhere to the following interfaces: + +```php + 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', +]); +``` + +The above options array corresponds to the defaults. By replacing `GroupCountBased` by +`GroupPosBased` you could switch to a different dispatching strategy. + +### A Note on HEAD Requests + +The HTTP spec requires servers to [support both GET and HEAD methods][2616-511]: + +> The methods GET and HEAD MUST be supported by all general-purpose servers + +To avoid forcing users to manually register HEAD routes for each resource we fallback to matching an +available GET route for a given resource. The PHP web SAPI transparently removes the entity body +from HEAD responses so this behavior has no effect on the vast majority of users. + +However, implementers using FastRoute outside the web SAPI environment (e.g. a custom server) MUST +NOT send entity bodies generated in response to HEAD requests. If you are a non-SAPI user this is +*your responsibility*; FastRoute has no purview to prevent you from breaking HTTP in such cases. + +Finally, note that applications MAY always specify their own HEAD method route for a given +resource to bypass this behavior entirely. + +### Credits + +This library is based on a router that [Levi Morrison][levi] implemented for the Aerys server. + +A large number of tests, as well as HTTP compliance considerations, were provided by [Daniel Lowrey][rdlowrey]. + + +[2616-511]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.1 "RFC 2616 Section 5.1.1" +[blog_post]: http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html +[levi]: https://github.com/morrisonlevi +[rdlowrey]: https://github.com/rdlowrey diff --git a/vendor/nikic/fast-route/composer.json b/vendor/nikic/fast-route/composer.json new file mode 100644 index 0000000..fb446a2 --- /dev/null +++ b/vendor/nikic/fast-route/composer.json @@ -0,0 +1,24 @@ +{ + "name": "nikic/fast-route", + "description": "Fast request router for PHP", + "keywords": ["routing", "router"], + "license": "BSD-3-Clause", + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": ["src/functions.php"] + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + } +} diff --git a/vendor/nikic/fast-route/phpunit.xml b/vendor/nikic/fast-route/phpunit.xml new file mode 100644 index 0000000..3c807b6 --- /dev/null +++ b/vendor/nikic/fast-route/phpunit.xml @@ -0,0 +1,24 @@ + + + + + + ./test/ + + + + + + ./src/ + + + diff --git a/vendor/nikic/fast-route/psalm.xml b/vendor/nikic/fast-route/psalm.xml new file mode 100644 index 0000000..0dca5d7 --- /dev/null +++ b/vendor/nikic/fast-route/psalm.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/vendor/nikic/fast-route/src/BadRouteException.php b/vendor/nikic/fast-route/src/BadRouteException.php new file mode 100644 index 0000000..62262ec --- /dev/null +++ b/vendor/nikic/fast-route/src/BadRouteException.php @@ -0,0 +1,7 @@ + $route) { + $suffixLen++; + $suffix .= "\t"; + + $regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})'; + $routeMap[$suffix] = [$route->handler, $route->variables]; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'suffix' => '/' . $suffix, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php b/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php new file mode 100644 index 0000000..54d9a05 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/GroupCountBased.php @@ -0,0 +1,30 @@ + $route) { + $numVariables = count($route->variables); + $numGroups = max($numGroups, $numVariables); + + $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables); + $routeMap[$numGroups + 1] = [$route->handler, $route->variables]; + + ++$numGroups; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php b/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php new file mode 100644 index 0000000..fc4dc0a --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/GroupPosBased.php @@ -0,0 +1,27 @@ + $route) { + $regexes[] = $regex; + $routeMap[$offset] = [$route->handler, $route->variables]; + + $offset += count($route->variables); + } + + $regex = '~^(?:' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php b/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php new file mode 100644 index 0000000..0aebed9 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/MarkBased.php @@ -0,0 +1,27 @@ + $route) { + $regexes[] = $regex . '(*MARK:' . $markName . ')'; + $routeMap[$markName] = [$route->handler, $route->variables]; + + ++$markName; + } + + $regex = '~^(?|' . implode('|', $regexes) . ')$~'; + return ['regex' => $regex, 'routeMap' => $routeMap]; + } +} diff --git a/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php b/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php new file mode 100644 index 0000000..6457290 --- /dev/null +++ b/vendor/nikic/fast-route/src/DataGenerator/RegexBasedAbstract.php @@ -0,0 +1,186 @@ +isStaticRoute($routeData)) { + $this->addStaticRoute($httpMethod, $routeData, $handler); + } else { + $this->addVariableRoute($httpMethod, $routeData, $handler); + } + } + + /** + * @return mixed[] + */ + public function getData() + { + if (empty($this->methodToRegexToRoutesMap)) { + return [$this->staticRoutes, []]; + } + + return [$this->staticRoutes, $this->generateVariableRouteData()]; + } + + /** + * @return mixed[] + */ + private function generateVariableRouteData() + { + $data = []; + foreach ($this->methodToRegexToRoutesMap as $method => $regexToRoutesMap) { + $chunkSize = $this->computeChunkSize(count($regexToRoutesMap)); + $chunks = array_chunk($regexToRoutesMap, $chunkSize, true); + $data[$method] = array_map([$this, 'processChunk'], $chunks); + } + return $data; + } + + /** + * @param int + * @return int + */ + private function computeChunkSize($count) + { + $numParts = max(1, round($count / $this->getApproxChunkSize())); + return (int) ceil($count / $numParts); + } + + /** + * @param mixed[] + * @return bool + */ + private function isStaticRoute($routeData) + { + return count($routeData) === 1 && is_string($routeData[0]); + } + + private function addStaticRoute($httpMethod, $routeData, $handler) + { + $routeStr = $routeData[0]; + + if (isset($this->staticRoutes[$httpMethod][$routeStr])) { + throw new BadRouteException(sprintf( + 'Cannot register two routes matching "%s" for method "%s"', + $routeStr, $httpMethod + )); + } + + if (isset($this->methodToRegexToRoutesMap[$httpMethod])) { + foreach ($this->methodToRegexToRoutesMap[$httpMethod] as $route) { + if ($route->matches($routeStr)) { + throw new BadRouteException(sprintf( + 'Static route "%s" is shadowed by previously defined variable route "%s" for method "%s"', + $routeStr, $route->regex, $httpMethod + )); + } + } + } + + $this->staticRoutes[$httpMethod][$routeStr] = $handler; + } + + private function addVariableRoute($httpMethod, $routeData, $handler) + { + list($regex, $variables) = $this->buildRegexForRoute($routeData); + + if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) { + throw new BadRouteException(sprintf( + 'Cannot register two routes matching "%s" for method "%s"', + $regex, $httpMethod + )); + } + + $this->methodToRegexToRoutesMap[$httpMethod][$regex] = new Route( + $httpMethod, $handler, $regex, $variables + ); + } + + /** + * @param mixed[] + * @return mixed[] + */ + private function buildRegexForRoute($routeData) + { + $regex = ''; + $variables = []; + foreach ($routeData as $part) { + if (is_string($part)) { + $regex .= preg_quote($part, '~'); + continue; + } + + list($varName, $regexPart) = $part; + + if (isset($variables[$varName])) { + throw new BadRouteException(sprintf( + 'Cannot use the same placeholder "%s" twice', $varName + )); + } + + if ($this->regexHasCapturingGroups($regexPart)) { + throw new BadRouteException(sprintf( + 'Regex "%s" for parameter "%s" contains a capturing group', + $regexPart, $varName + )); + } + + $variables[$varName] = $varName; + $regex .= '(' . $regexPart . ')'; + } + + return [$regex, $variables]; + } + + /** + * @param string + * @return bool + */ + private function regexHasCapturingGroups($regex) + { + if (false === strpos($regex, '(')) { + // Needs to have at least a ( to contain a capturing group + return false; + } + + // Semi-accurate detection for capturing groups + return (bool) preg_match( + '~ + (?: + \(\?\( + | \[ [^\]\\\\]* (?: \\\\ . [^\]\\\\]* )* \] + | \\\\ . + ) (*SKIP)(*FAIL) | + \( + (?! + \? (?! <(?![!=]) | P< | \' ) + | \* + ) + ~x', + $regex + ); + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher.php b/vendor/nikic/fast-route/src/Dispatcher.php new file mode 100644 index 0000000..4ae72a3 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher.php @@ -0,0 +1,26 @@ + 'value', ...]] + * + * @param string $httpMethod + * @param string $uri + * + * @return array + */ + public function dispatch($httpMethod, $uri); +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php b/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php new file mode 100644 index 0000000..ef1eec1 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/CharCountBased.php @@ -0,0 +1,31 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri . $data['suffix'], $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][end($matches)]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php b/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php new file mode 100644 index 0000000..493e7a9 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/GroupCountBased.php @@ -0,0 +1,31 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][count($matches)]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php b/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php new file mode 100644 index 0000000..498220e --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/GroupPosBased.php @@ -0,0 +1,33 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + // find first non-empty match + for ($i = 1; '' === $matches[$i]; ++$i); + + list($handler, $varNames) = $data['routeMap'][$i]; + + $vars = []; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[$i++]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php b/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php new file mode 100644 index 0000000..22eb09b --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/MarkBased.php @@ -0,0 +1,31 @@ +staticRouteMap, $this->variableRouteData) = $data; + } + + protected function dispatchVariableRoute($routeData, $uri) + { + foreach ($routeData as $data) { + if (!preg_match($data['regex'], $uri, $matches)) { + continue; + } + + list($handler, $varNames) = $data['routeMap'][$matches['MARK']]; + + $vars = []; + $i = 0; + foreach ($varNames as $varName) { + $vars[$varName] = $matches[++$i]; + } + return [self::FOUND, $handler, $vars]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php b/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php new file mode 100644 index 0000000..206e879 --- /dev/null +++ b/vendor/nikic/fast-route/src/Dispatcher/RegexBasedAbstract.php @@ -0,0 +1,88 @@ +staticRouteMap[$httpMethod][$uri])) { + $handler = $this->staticRouteMap[$httpMethod][$uri]; + return [self::FOUND, $handler, []]; + } + + $varRouteData = $this->variableRouteData; + if (isset($varRouteData[$httpMethod])) { + $result = $this->dispatchVariableRoute($varRouteData[$httpMethod], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + + // For HEAD requests, attempt fallback to GET + if ($httpMethod === 'HEAD') { + if (isset($this->staticRouteMap['GET'][$uri])) { + $handler = $this->staticRouteMap['GET'][$uri]; + return [self::FOUND, $handler, []]; + } + if (isset($varRouteData['GET'])) { + $result = $this->dispatchVariableRoute($varRouteData['GET'], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + } + + // If nothing else matches, try fallback routes + if (isset($this->staticRouteMap['*'][$uri])) { + $handler = $this->staticRouteMap['*'][$uri]; + return [self::FOUND, $handler, []]; + } + if (isset($varRouteData['*'])) { + $result = $this->dispatchVariableRoute($varRouteData['*'], $uri); + if ($result[0] === self::FOUND) { + return $result; + } + } + + // Find allowed methods for this URI by matching against all other HTTP methods as well + $allowedMethods = []; + + foreach ($this->staticRouteMap as $method => $uriMap) { + if ($method !== $httpMethod && isset($uriMap[$uri])) { + $allowedMethods[] = $method; + } + } + + foreach ($varRouteData as $method => $routeData) { + if ($method === $httpMethod) { + continue; + } + + $result = $this->dispatchVariableRoute($routeData, $uri); + if ($result[0] === self::FOUND) { + $allowedMethods[] = $method; + } + } + + // If there are no allowed methods the route simply does not exist + if ($allowedMethods) { + return [self::METHOD_NOT_ALLOWED, $allowedMethods]; + } + + return [self::NOT_FOUND]; + } +} diff --git a/vendor/nikic/fast-route/src/Route.php b/vendor/nikic/fast-route/src/Route.php new file mode 100644 index 0000000..e1bf7dd --- /dev/null +++ b/vendor/nikic/fast-route/src/Route.php @@ -0,0 +1,47 @@ +httpMethod = $httpMethod; + $this->handler = $handler; + $this->regex = $regex; + $this->variables = $variables; + } + + /** + * Tests whether this route matches the given string. + * + * @param string $str + * + * @return bool + */ + public function matches($str) + { + $regex = '~^' . $this->regex . '$~'; + return (bool) preg_match($regex, $str); + } +} diff --git a/vendor/nikic/fast-route/src/RouteCollector.php b/vendor/nikic/fast-route/src/RouteCollector.php new file mode 100644 index 0000000..c1c1762 --- /dev/null +++ b/vendor/nikic/fast-route/src/RouteCollector.php @@ -0,0 +1,152 @@ +routeParser = $routeParser; + $this->dataGenerator = $dataGenerator; + $this->currentGroupPrefix = ''; + } + + /** + * Adds a route to the collection. + * + * The syntax used in the $route string depends on the used route parser. + * + * @param string|string[] $httpMethod + * @param string $route + * @param mixed $handler + */ + public function addRoute($httpMethod, $route, $handler) + { + $route = $this->currentGroupPrefix . $route; + $routeDatas = $this->routeParser->parse($route); + foreach ((array) $httpMethod as $method) { + foreach ($routeDatas as $routeData) { + $this->dataGenerator->addRoute($method, $routeData, $handler); + } + } + } + + /** + * Create a route group with a common prefix. + * + * All routes created in the passed callback will have the given group prefix prepended. + * + * @param string $prefix + * @param callable $callback + */ + public function addGroup($prefix, callable $callback) + { + $previousGroupPrefix = $this->currentGroupPrefix; + $this->currentGroupPrefix = $previousGroupPrefix . $prefix; + $callback($this); + $this->currentGroupPrefix = $previousGroupPrefix; + } + + /** + * Adds a GET route to the collection + * + * This is simply an alias of $this->addRoute('GET', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function get($route, $handler) + { + $this->addRoute('GET', $route, $handler); + } + + /** + * Adds a POST route to the collection + * + * This is simply an alias of $this->addRoute('POST', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function post($route, $handler) + { + $this->addRoute('POST', $route, $handler); + } + + /** + * Adds a PUT route to the collection + * + * This is simply an alias of $this->addRoute('PUT', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function put($route, $handler) + { + $this->addRoute('PUT', $route, $handler); + } + + /** + * Adds a DELETE route to the collection + * + * This is simply an alias of $this->addRoute('DELETE', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function delete($route, $handler) + { + $this->addRoute('DELETE', $route, $handler); + } + + /** + * Adds a PATCH route to the collection + * + * This is simply an alias of $this->addRoute('PATCH', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function patch($route, $handler) + { + $this->addRoute('PATCH', $route, $handler); + } + + /** + * Adds a HEAD route to the collection + * + * This is simply an alias of $this->addRoute('HEAD', $route, $handler) + * + * @param string $route + * @param mixed $handler + */ + public function head($route, $handler) + { + $this->addRoute('HEAD', $route, $handler); + } + + /** + * Returns the collected route data, as provided by the data generator. + * + * @return array + */ + public function getData() + { + return $this->dataGenerator->getData(); + } +} diff --git a/vendor/nikic/fast-route/src/RouteParser.php b/vendor/nikic/fast-route/src/RouteParser.php new file mode 100644 index 0000000..6a7685c --- /dev/null +++ b/vendor/nikic/fast-route/src/RouteParser.php @@ -0,0 +1,37 @@ + $segment) { + if ($segment === '' && $n !== 0) { + throw new BadRouteException('Empty optional part'); + } + + $currentRoute .= $segment; + $routeDatas[] = $this->parsePlaceholders($currentRoute); + } + return $routeDatas; + } + + /** + * Parses a route string that does not contain optional segments. + * + * @param string + * @return mixed[] + */ + private function parsePlaceholders($route) + { + if (!preg_match_all( + '~' . self::VARIABLE_REGEX . '~x', $route, $matches, + PREG_OFFSET_CAPTURE | PREG_SET_ORDER + )) { + return [$route]; + } + + $offset = 0; + $routeData = []; + foreach ($matches as $set) { + if ($set[0][1] > $offset) { + $routeData[] = substr($route, $offset, $set[0][1] - $offset); + } + $routeData[] = [ + $set[1][0], + isset($set[2]) ? trim($set[2][0]) : self::DEFAULT_DISPATCH_REGEX + ]; + $offset = $set[0][1] + strlen($set[0][0]); + } + + if ($offset !== strlen($route)) { + $routeData[] = substr($route, $offset); + } + + return $routeData; + } +} diff --git a/vendor/nikic/fast-route/src/bootstrap.php b/vendor/nikic/fast-route/src/bootstrap.php new file mode 100644 index 0000000..0bce3a4 --- /dev/null +++ b/vendor/nikic/fast-route/src/bootstrap.php @@ -0,0 +1,12 @@ + 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + ]; + + /** @var RouteCollector $routeCollector */ + $routeCollector = new $options['routeCollector']( + new $options['routeParser'], new $options['dataGenerator'] + ); + $routeDefinitionCallback($routeCollector); + + return new $options['dispatcher']($routeCollector->getData()); + } + + /** + * @param callable $routeDefinitionCallback + * @param array $options + * + * @return Dispatcher + */ + function cachedDispatcher(callable $routeDefinitionCallback, array $options = []) + { + $options += [ + 'routeParser' => 'FastRoute\\RouteParser\\Std', + 'dataGenerator' => 'FastRoute\\DataGenerator\\GroupCountBased', + 'dispatcher' => 'FastRoute\\Dispatcher\\GroupCountBased', + 'routeCollector' => 'FastRoute\\RouteCollector', + 'cacheDisabled' => false, + ]; + + if (!isset($options['cacheFile'])) { + throw new \LogicException('Must specify "cacheFile" option'); + } + + if (!$options['cacheDisabled'] && file_exists($options['cacheFile'])) { + $dispatchData = require $options['cacheFile']; + if (!is_array($dispatchData)) { + throw new \RuntimeException('Invalid cache file "' . $options['cacheFile'] . '"'); + } + return new $options['dispatcher']($dispatchData); + } + + $routeCollector = new $options['routeCollector']( + new $options['routeParser'], new $options['dataGenerator'] + ); + $routeDefinitionCallback($routeCollector); + + /** @var RouteCollector $routeCollector */ + $dispatchData = $routeCollector->getData(); + if (!$options['cacheDisabled']) { + file_put_contents( + $options['cacheFile'], + ' $this->getDataGeneratorClass(), + 'dispatcher' => $this->getDispatcherClass() + ]; + } + + /** + * @dataProvider provideFoundDispatchCases + */ + public function testFoundDispatches($method, $uri, $callback, $handler, $argDict) + { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $info = $dispatcher->dispatch($method, $uri); + $this->assertSame($dispatcher::FOUND, $info[0]); + $this->assertSame($handler, $info[1]); + $this->assertSame($argDict, $info[2]); + } + + /** + * @dataProvider provideNotFoundDispatchCases + */ + public function testNotFoundDispatches($method, $uri, $callback) + { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $routeInfo = $dispatcher->dispatch($method, $uri); + $this->assertArrayNotHasKey(1, $routeInfo, + 'NOT_FOUND result must only contain a single element in the returned info array' + ); + $this->assertSame($dispatcher::NOT_FOUND, $routeInfo[0]); + } + + /** + * @dataProvider provideMethodNotAllowedDispatchCases + */ + public function testMethodNotAllowedDispatches($method, $uri, $callback, $availableMethods) + { + $dispatcher = \FastRoute\simpleDispatcher($callback, $this->generateDispatcherOptions()); + $routeInfo = $dispatcher->dispatch($method, $uri); + $this->assertArrayHasKey(1, $routeInfo, + 'METHOD_NOT_ALLOWED result must return an array of allowed methods at index 1' + ); + + list($routedStatus, $methodArray) = $dispatcher->dispatch($method, $uri); + $this->assertSame($dispatcher::METHOD_NOT_ALLOWED, $routedStatus); + $this->assertSame($availableMethods, $methodArray); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot use the same placeholder "test" twice + */ + public function testDuplicateVariableNameError() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/foo/{test}/{test:\d+}', 'handler0'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot register two routes matching "/user/([^/]+)" for method "GET" + */ + public function testDuplicateVariableRoute() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/user/{id}', 'handler0'); // oops, forgot \d+ restriction ;) + $r->addRoute('GET', '/user/{name}', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Cannot register two routes matching "/user" for method "GET" + */ + public function testDuplicateStaticRoute() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/user', 'handler0'); + $r->addRoute('GET', '/user', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Static route "/user/nikic" is shadowed by previously defined variable route "/user/([^/]+)" for method "GET" + */ + public function testShadowedStaticRoute() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/nikic', 'handler1'); + }, $this->generateDispatcherOptions()); + } + + /** + * @expectedException \FastRoute\BadRouteException + * @expectedExceptionMessage Regex "(en|de)" for parameter "lang" contains a capturing group + */ + public function testCapturing() + { + \FastRoute\simpleDispatcher(function (RouteCollector $r) { + $r->addRoute('GET', '/{lang:(en|de)}', 'handler0'); + }, $this->generateDispatcherOptions()); + } + + public function provideFoundDispatchCases() + { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'GET'; + $uri = '/resource/123/456'; + $handler = 'handler0'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 1 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/handler0', 'handler0'); + $r->addRoute('GET', '/handler1', 'handler1'); + $r->addRoute('GET', '/handler2', 'handler2'); + }; + + $method = 'GET'; + $uri = '/handler2'; + $handler = 'handler2'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 2 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey'; + $handler = 'handler2'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 3 --------------------------------------------------------------------------------------> + + // reuse $callback from #2 + + $method = 'GET'; + $uri = '/user/12345'; + $handler = 'handler1'; + $argDict = ['id' => '12345']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 4 --------------------------------------------------------------------------------------> + + // reuse $callback from #3 + + $method = 'GET'; + $uri = '/user/NaN'; + $handler = 'handler2'; + $argDict = ['name' => 'NaN']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 5 --------------------------------------------------------------------------------------> + + // reuse $callback from #4 + + $method = 'GET'; + $uri = '/user/rdlowrey/12345'; + $handler = 'handler0'; + $argDict = ['name' => 'rdlowrey', 'id' => '12345']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 6 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/12345/extension', 'handler1'); + $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/user/12345.svg'; + $handler = 'handler2'; + $argDict = ['id' => '12345', 'extension' => 'svg']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 7 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/static0', 'handler2'); + $r->addRoute('GET', '/static1', 'handler3'); + $r->addRoute('HEAD', '/static1', 'handler4'); + }; + + $method = 'HEAD'; + $uri = '/user/rdlowrey'; + $handler = 'handler0'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 8 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + // reuse $callback from #7 + + $method = 'HEAD'; + $uri = '/user/rdlowrey/1234'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey', 'id' => '1234']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 9 ----- Test GET method fallback on HEAD route miss ------------------------------------> + + // reuse $callback from #8 + + $method = 'HEAD'; + $uri = '/static0'; + $handler = 'handler2'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 10 ---- Test existing HEAD route used if available (no fallback) -----------------------> + + // reuse $callback from #9 + + $method = 'HEAD'; + $uri = '/static1'; + $handler = 'handler4'; + $argDict = []; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 11 ---- More specified routes are not shadowed by less specific of another method ------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); + }; + + $method = 'POST'; + $uri = '/user/rdlowrey'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 12 ---- Handler of more specific routes is used, if it occurs first --------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); + $r->addRoute('POST', '/user/{name}', 'handler2'); + }; + + $method = 'POST'; + $uri = '/user/rdlowrey'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 13 ---- Route with constant suffix -----------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}', 'handler0'); + $r->addRoute('GET', '/user/{name}/edit', 'handler1'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey/edit'; + $handler = 'handler1'; + $argDict = ['name' => 'rdlowrey']; + + $cases[] = [$method, $uri, $callback, $handler, $argDict]; + + // 14 ---- Handle multiple methods with the same handler ----------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); + $r->addRoute(['DELETE'], '/user', 'handlerDelete'); + $r->addRoute([], '/user', 'handlerNone'); + }; + + $argDict = []; + $cases[] = ['GET', '/user', $callback, 'handlerGetPost', $argDict]; + $cases[] = ['POST', '/user', $callback, 'handlerGetPost', $argDict]; + $cases[] = ['DELETE', '/user', $callback, 'handlerDelete', $argDict]; + + // 17 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('POST', '/user.json', 'handler0'); + $r->addRoute('GET', '/{entity}.json', 'handler1'); + }; + + $cases[] = ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']]; + + // 18 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '', 'handler0'); + }; + + $cases[] = ['GET', '', $callback, 'handler0', []]; + + // 19 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('HEAD', '/a/{foo}', 'handler0'); + $r->addRoute('GET', '/b/{foo}', 'handler1'); + }; + + $cases[] = ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']]; + + // 20 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('HEAD', '/a', 'handler0'); + $r->addRoute('GET', '/b', 'handler1'); + }; + + $cases[] = ['HEAD', '/b', $callback, 'handler1', []]; + + // 21 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/foo', 'handler0'); + $r->addRoute('HEAD', '/{bar}', 'handler1'); + }; + + $cases[] = ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']]; + + // 22 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('*', '/user', 'handler0'); + $r->addRoute('*', '/{user}', 'handler1'); + $r->addRoute('GET', '/user', 'handler2'); + }; + + $cases[] = ['GET', '/user', $callback, 'handler2', []]; + + // 23 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('*', '/user', 'handler0'); + $r->addRoute('GET', '/user', 'handler1'); + }; + + $cases[] = ['POST', '/user', $callback, 'handler0', []]; + + // 24 ---- + + $cases[] = ['HEAD', '/user', $callback, 'handler1', []]; + + // 25 ---- + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/{bar}', 'handler0'); + $r->addRoute('*', '/foo', 'handler1'); + }; + + $cases[] = ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']]; + + // 26 ---- + + $callback = function(RouteCollector $r) { + $r->addRoute('GET', '/user', 'handler0'); + $r->addRoute('*', '/{foo:.*}', 'handler1'); + }; + + $cases[] = ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + + public function provideNotFoundDispatchCases() + { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 1 --------------------------------------------------------------------------------------> + + // reuse callback from #0 + $method = 'POST'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 2 --------------------------------------------------------------------------------------> + + // reuse callback from #1 + $method = 'PUT'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 3 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/handler0', 'handler0'); + $r->addRoute('GET', '/handler1', 'handler1'); + $r->addRoute('GET', '/handler2', 'handler2'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 4 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler1'); + $r->addRoute('GET', '/user/{name}', 'handler2'); + }; + + $method = 'GET'; + $uri = '/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 5 --------------------------------------------------------------------------------------> + + // reuse callback from #4 + $method = 'GET'; + $uri = '/user/rdlowrey/12345/not-found'; + + $cases[] = [$method, $uri, $callback]; + + // 6 --------------------------------------------------------------------------------------> + + // reuse callback from #5 + $method = 'HEAD'; + + $cases[] = [$method, $uri, $callback]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } + + public function provideMethodNotAllowedDispatchCases() + { + $cases = []; + + // 0 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + }; + + $method = 'POST'; + $uri = '/resource/123/456'; + $allowedMethods = ['GET']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 1 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/resource/123/456', 'handler0'); + $r->addRoute('POST', '/resource/123/456', 'handler1'); + $r->addRoute('PUT', '/resource/123/456', 'handler2'); + $r->addRoute('*', '/', 'handler3'); + }; + + $method = 'DELETE'; + $uri = '/resource/123/456'; + $allowedMethods = ['GET', 'POST', 'PUT']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 2 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); + $r->addRoute('POST', '/user/{name}/{id:[0-9]+}', 'handler1'); + $r->addRoute('PUT', '/user/{name}/{id:[0-9]+}', 'handler2'); + $r->addRoute('PATCH', '/user/{name}/{id:[0-9]+}', 'handler3'); + }; + + $method = 'DELETE'; + $uri = '/user/rdlowrey/42'; + $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 3 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute('POST', '/user/{name}', 'handler1'); + $r->addRoute('PUT', '/user/{name:[a-z]+}', 'handler2'); + $r->addRoute('PATCH', '/user/{name:[a-z]+}', 'handler3'); + }; + + $method = 'GET'; + $uri = '/user/rdlowrey'; + $allowedMethods = ['POST', 'PUT', 'PATCH']; + + $cases[] = [$method, $uri, $callback, $allowedMethods]; + + // 4 --------------------------------------------------------------------------------------> + + $callback = function (RouteCollector $r) { + $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); + $r->addRoute(['DELETE'], '/user', 'handlerDelete'); + $r->addRoute([], '/user', 'handlerNone'); + }; + + $cases[] = ['PUT', '/user', $callback, ['GET', 'POST', 'DELETE']]; + + // 5 + + $callback = function (RouteCollector $r) { + $r->addRoute('POST', '/user.json', 'handler0'); + $r->addRoute('GET', '/{entity}.json', 'handler1'); + }; + + $cases[] = ['PUT', '/user.json', $callback, ['POST', 'GET']]; + + // x --------------------------------------------------------------------------------------> + + return $cases; + } +} diff --git a/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php b/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php new file mode 100644 index 0000000..f821ef5 --- /dev/null +++ b/vendor/nikic/fast-route/test/Dispatcher/GroupCountBasedTest.php @@ -0,0 +1,16 @@ +markTestSkipped('PHP 5.6 required for MARK support'); + } + } + + protected function getDispatcherClass() + { + return 'FastRoute\\Dispatcher\\MarkBased'; + } + + protected function getDataGeneratorClass() + { + return 'FastRoute\\DataGenerator\\MarkBased'; + } +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/HackTypecheckerTest.php b/vendor/nikic/fast-route/test/HackTypechecker/HackTypecheckerTest.php new file mode 100644 index 0000000..b6fc53f --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/HackTypecheckerTest.php @@ -0,0 +1,44 @@ +markTestSkipped('HHVM only'); + } + if (!version_compare(HHVM_VERSION, '3.9.0', '>=')) { + $this->markTestSkipped('classname requires HHVM 3.9+'); + } + + // The typechecker recurses the whole tree, so it makes sure + // that everything in fixtures/ is valid when this runs. + + $output = []; + $exit_code = null; + exec( + 'hh_server --check ' . escapeshellarg(__DIR__ . '/../../') . ' 2>&1', + $output, + $exit_code + ); + if ($exit_code === self::SERVER_ALREADY_RUNNING_CODE) { + $this->assertTrue( + $recurse, + 'Typechecker still running after running hh_client stop' + ); + // Server already running - 3.10 => 3.11 regression: + // https://github.com/facebook/hhvm/issues/6646 + exec('hh_client stop 2>/dev/null'); + $this->testTypechecks(/* recurse = */ false); + return; + + } + $this->assertSame(0, $exit_code, implode("\n", $output)); + } +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/fixtures/all_options.php b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/all_options.php new file mode 100644 index 0000000..05a9af2 --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/all_options.php @@ -0,0 +1,29 @@ + {}, + shape( + 'routeParser' => \FastRoute\RouteParser\Std::class, + 'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class, + 'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class, + 'routeCollector' => \FastRoute\RouteCollector::class, + ), + ); +} + +function all_options_cached(): \FastRoute\Dispatcher { + return \FastRoute\cachedDispatcher( + $collector ==> {}, + shape( + 'routeParser' => \FastRoute\RouteParser\Std::class, + 'dataGenerator' => \FastRoute\DataGenerator\GroupCountBased::class, + 'dispatcher' => \FastRoute\Dispatcher\GroupCountBased::class, + 'routeCollector' => \FastRoute\RouteCollector::class, + 'cacheFile' => '/dev/null', + 'cacheDisabled' => false, + ), + ); +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/fixtures/empty_options.php b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/empty_options.php new file mode 100644 index 0000000..61eb541 --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/empty_options.php @@ -0,0 +1,11 @@ + {}, shape()); +} + +function empty_options_cached(): \FastRoute\Dispatcher { + return \FastRoute\cachedDispatcher($collector ==> {}, shape()); +} diff --git a/vendor/nikic/fast-route/test/HackTypechecker/fixtures/no_options.php b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/no_options.php new file mode 100644 index 0000000..44b5422 --- /dev/null +++ b/vendor/nikic/fast-route/test/HackTypechecker/fixtures/no_options.php @@ -0,0 +1,11 @@ + {}); +} + +function no_options_cached(): \FastRoute\Dispatcher { + return \FastRoute\cachedDispatcher($collector ==> {}); +} diff --git a/vendor/nikic/fast-route/test/RouteCollectorTest.php b/vendor/nikic/fast-route/test/RouteCollectorTest.php new file mode 100644 index 0000000..cc54407 --- /dev/null +++ b/vendor/nikic/fast-route/test/RouteCollectorTest.php @@ -0,0 +1,108 @@ +delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + + $expected = [ + ['DELETE', '/delete', 'delete'], + ['GET', '/get', 'get'], + ['HEAD', '/head', 'head'], + ['PATCH', '/patch', 'patch'], + ['POST', '/post', 'post'], + ['PUT', '/put', 'put'], + ]; + + $this->assertSame($expected, $r->routes); + } + + public function testGroups() + { + $r = new DummyRouteCollector(); + + $r->delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + + $r->addGroup('/group-one', function (DummyRouteCollector $r) { + $r->delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + + $r->addGroup('/group-two', function (DummyRouteCollector $r) { + $r->delete('/delete', 'delete'); + $r->get('/get', 'get'); + $r->head('/head', 'head'); + $r->patch('/patch', 'patch'); + $r->post('/post', 'post'); + $r->put('/put', 'put'); + }); + }); + + $r->addGroup('/admin', function (DummyRouteCollector $r) { + $r->get('-some-info', 'admin-some-info'); + }); + $r->addGroup('/admin-', function (DummyRouteCollector $r) { + $r->get('more-info', 'admin-more-info'); + }); + + $expected = [ + ['DELETE', '/delete', 'delete'], + ['GET', '/get', 'get'], + ['HEAD', '/head', 'head'], + ['PATCH', '/patch', 'patch'], + ['POST', '/post', 'post'], + ['PUT', '/put', 'put'], + ['DELETE', '/group-one/delete', 'delete'], + ['GET', '/group-one/get', 'get'], + ['HEAD', '/group-one/head', 'head'], + ['PATCH', '/group-one/patch', 'patch'], + ['POST', '/group-one/post', 'post'], + ['PUT', '/group-one/put', 'put'], + ['DELETE', '/group-one/group-two/delete', 'delete'], + ['GET', '/group-one/group-two/get', 'get'], + ['HEAD', '/group-one/group-two/head', 'head'], + ['PATCH', '/group-one/group-two/patch', 'patch'], + ['POST', '/group-one/group-two/post', 'post'], + ['PUT', '/group-one/group-two/put', 'put'], + ['GET', '/admin-some-info', 'admin-some-info'], + ['GET', '/admin-more-info', 'admin-more-info'], + ]; + + $this->assertSame($expected, $r->routes); + } +} + +class DummyRouteCollector extends RouteCollector +{ + public $routes = []; + + public function __construct() + { + } + + public function addRoute($method, $route, $handler) + { + $route = $this->currentGroupPrefix . $route; + $this->routes[] = [$method, $route, $handler]; + } +} diff --git a/vendor/nikic/fast-route/test/RouteParser/StdTest.php b/vendor/nikic/fast-route/test/RouteParser/StdTest.php new file mode 100644 index 0000000..e13e4de --- /dev/null +++ b/vendor/nikic/fast-route/test/RouteParser/StdTest.php @@ -0,0 +1,154 @@ +parse($routeString); + $this->assertSame($expectedRouteDatas, $routeDatas); + } + + /** @dataProvider provideTestParseError */ + public function testParseError($routeString, $expectedExceptionMessage) + { + $parser = new Std(); + $this->setExpectedException('FastRoute\\BadRouteException', $expectedExceptionMessage); + $parser->parse($routeString); + } + + public function provideTestParse() + { + return [ + [ + '/test', + [ + ['/test'], + ] + ], + [ + '/test/{param}', + [ + ['/test/', ['param', '[^/]+']], + ] + ], + [ + '/te{ param }st', + [ + ['/te', ['param', '[^/]+'], 'st'] + ] + ], + [ + '/test/{param1}/test2/{param2}', + [ + ['/test/', ['param1', '[^/]+'], '/test2/', ['param2', '[^/]+']] + ] + ], + [ + '/test/{param:\d+}', + [ + ['/test/', ['param', '\d+']] + ] + ], + [ + '/test/{ param : \d{1,9} }', + [ + ['/test/', ['param', '\d{1,9}']] + ] + ], + [ + '/test[opt]', + [ + ['/test'], + ['/testopt'], + ] + ], + [ + '/test[/{param}]', + [ + ['/test'], + ['/test/', ['param', '[^/]+']], + ] + ], + [ + '/{param}[opt]', + [ + ['/', ['param', '[^/]+']], + ['/', ['param', '[^/]+'], 'opt'] + ] + ], + [ + '/test[/{name}[/{id:[0-9]+}]]', + [ + ['/test'], + ['/test/', ['name', '[^/]+']], + ['/test/', ['name', '[^/]+'], '/', ['id', '[0-9]+']], + ] + ], + [ + '', + [ + [''], + ] + ], + [ + '[test]', + [ + [''], + ['test'], + ] + ], + [ + '/{foo-bar}', + [ + ['/', ['foo-bar', '[^/]+']] + ] + ], + [ + '/{_foo:.*}', + [ + ['/', ['_foo', '.*']] + ] + ], + ]; + } + + public function provideTestParseError() + { + return [ + [ + '/test[opt', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/test[opt[opt2]', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/testopt]', + "Number of opening '[' and closing ']' does not match" + ], + [ + '/test[]', + 'Empty optional part' + ], + [ + '/test[[opt]]', + 'Empty optional part' + ], + [ + '[[test]]', + 'Empty optional part' + ], + [ + '/test[/opt]/required', + 'Optional segments can only occur at the end of a route' + ], + ]; + } +} diff --git a/vendor/nikic/fast-route/test/bootstrap.php b/vendor/nikic/fast-route/test/bootstrap.php new file mode 100644 index 0000000..3023f41 --- /dev/null +++ b/vendor/nikic/fast-route/test/bootstrap.php @@ -0,0 +1,11 @@ + `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + diff --git a/vendor/psr/http-message/docs/PSR7-Usage.md b/vendor/psr/http-message/docs/PSR7-Usage.md new file mode 100644 index 0000000..b6d048a --- /dev/null +++ b/vendor/psr/http-message/docs/PSR7-Usage.md @@ -0,0 +1,159 @@ +### PSR-7 Usage + +All PSR-7 applications comply with these interfaces +They were created to establish a standard between middleware implementations. + +> `RequestInterface`, `ServerRequestInterface`, `ResponseInterface` extend `MessageInterface` because the `Request` and the `Response` are `HTTP Messages`. +> When using `ServerRequestInterface`, both `RequestInterface` and `Psr\Http\Message\MessageInterface` methods are considered. + + +The following examples will illustrate how basic operations are done in PSR-7. + +##### Examples + + +For this examples to work (at least) a PSR-7 implementation package is required. (eg: zendframework/zend-diactoros, guzzlehttp/psr7, slim/slim, etc) +All PSR-7 implementations should have the same behaviour. + +The following will be assumed: +`$request` is an object of `Psr\Http\Message\RequestInterface` and + +`$response` is an object implementing `Psr\Http\Message\RequestInterface` + + +### Working with HTTP Headers + +#### Adding headers to response: + +```php +$response->withHeader('My-Custom-Header', 'My Custom Message'); +``` + +#### Appending values to headers + +```php +$response->withAddedHeader('My-Custom-Header', 'The second message'); +``` + +#### Checking if header exists: + +```php +$request->hasHeader('My-Custom-Header'); // will return false +$response->hasHeader('My-Custom-Header'); // will return true +``` + +> Note: My-Custom-Header was only added in the Response + +#### Getting comma-separated values from a header (also applies to request) + +```php +// getting value from request headers +$request->getHeaderLine('Content-Type'); // will return: "text/html; charset=UTF-8" +// getting value from response headers +$response->getHeaderLine('My-Custom-Header'); // will return: "My Custom Message; The second message" +``` + +#### Getting array of value from a header (also applies to request) +```php +// getting value from request headers +$request->getHeader('Content-Type'); // will return: ["text/html", "charset=UTF-8"] +// getting value from response headers +$response->getHeader('My-Custom-Header'); // will return: ["My Custom Message", "The second message"] +``` + +#### Removing headers from HTTP Messages +```php +// removing a header from Request, removing deprecated "Content-MD5" header +$request->withoutHeader('Content-MD5'); + +// removing a header from Response +// effect: the browser won't know the size of the stream +// the browser will download the stream till it ends +$response->withoutHeader('Content-Length'); +``` + +### Working with HTTP Message Body + +When working with the PSR-7 there are two methods of implementation: +#### 1. Getting the body separately + +> This method makes the body handling easier to understand and is useful when repeatedly calling body methods. (You only call `getBody()` once). Using this method mistakes like `$response->write()` are also prevented. + +```php +$body = $response->getBody(); +// operations on body, eg. read, write, seek +// ... +// replacing the old body +$response->withBody($body); +// this last statement is optional as we working with objects +// in this case the "new" body is same with the "old" one +// the $body variable has the same value as the one in $request, only the reference is passed +``` + +#### 2. Working directly on response + +> This method is useful when only performing few operations as the `$request->getBody()` statement fragment is required + +```php +$response->getBody()->write('hello'); +``` + +### Getting the body contents + +The following snippet gets the contents of a stream contents. +> Note: Streams must be rewinded, if content was written into streams, it will be ignored when calling `getContents()` because the stream pointer is set to the last character, which is `\0` - meaning end of stream. +```php +$body = $response->getBody(); +$body->rewind(); // or $body->seek(0); +$bodyText = $body->getContents(); +``` +> Note: If `$body->seek(1)` is called before `$body->getContents()`, the first character will be ommited as the starting pointer is set to `1`, not `0`. This is why using `$body->rewind()` is recommended. + +### Append to body + +```php +$response->getBody()->write('Hello'); // writing directly +$body = $request->getBody(); // which is a `StreamInterface` +$body->write('xxxxx'); +``` + +### Prepend to body +Prepending is different when it comes to streams. The content must be copied before writing the content to be prepended. +The following example will explain the behaviour of streams. + +```php +// assuming our response is initially empty +$body = $repsonse->getBody(); +// writing the string "abcd" +$body->write('abcd'); + +// seeking to start of stream +$body->seek(0); +// writing 'ef' +$body->write('ef'); // at this point the stream contains "efcd" +``` + +#### Prepending by rewriting separately + +```php +// assuming our response body stream only contains: "abcd" +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // abcd +// seeking the stream to beginning +$body->rewind(); +$body->write('ef'); // stream contains "efcd" +$body->write($contents); // stream contains "efabcd" +``` + +> Note: `getContents()` seeks the stream while reading it, therefore if the second `rewind()` method call was not present the stream would have resulted in `abcdefabcd` because the `write()` method appends to stream if not preceeded by `rewind()` or `seek(0)`. + +#### Prepending by using contents as a string +```php +$body = $response->getBody(); +$body->rewind(); +$contents = $body->getContents(); // efabcd +$contents = 'ef'.$contents; +$body->rewind(); +$body->write($contents); +``` diff --git a/vendor/psr/http-message/src/MessageInterface.php b/vendor/psr/http-message/src/MessageInterface.php new file mode 100644 index 0000000..8cdb4ed --- /dev/null +++ b/vendor/psr/http-message/src/MessageInterface.php @@ -0,0 +1,189 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(); + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader(string $name); + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader(string $name); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine(string $name); + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader(string $name, $value); + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader(string $name, $value); + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader(string $name); + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(); + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body); +} diff --git a/vendor/psr/http-message/src/RequestInterface.php b/vendor/psr/http-message/src/RequestInterface.php new file mode 100644 index 0000000..38066df --- /dev/null +++ b/vendor/psr/http-message/src/RequestInterface.php @@ -0,0 +1,131 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(); + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query); + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(); + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles); + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data); + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(); + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute(string $name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute(string $name, $value); + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute(string $name); +} diff --git a/vendor/psr/http-message/src/StreamInterface.php b/vendor/psr/http-message/src/StreamInterface.php new file mode 100644 index 0000000..5924663 --- /dev/null +++ b/vendor/psr/http-message/src/StreamInterface.php @@ -0,0 +1,160 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(); + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(); + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(); + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(); + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(); + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(); + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(); + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme(string $scheme); + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo(string $user, ?string $password = null); + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost(string $host); + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort(?int $port); + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath(string $path); + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery(string $query); + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment(string $fragment); + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(); +} diff --git a/vendor/react/async/CHANGELOG.md b/vendor/react/async/CHANGELOG.md new file mode 100644 index 0000000..bafac9d --- /dev/null +++ b/vendor/react/async/CHANGELOG.md @@ -0,0 +1,112 @@ +# Changelog + +## 4.3.0 (2024-06-04) + +* Feature: Improve performance by avoiding unneeded references in `FiberMap`. + (#88 by @clue) + +* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations. + (#87 by @clue) + +* Improve type safety for test environment. + (#86 by @SimonFrings) + +## 4.2.0 (2023-11-22) + +* Feature: Add Promise v3 template types for all public functions. + (#40 by @WyriHaximus and @clue) + + All our public APIs now use Promise v3 template types to guide IDEs and static + analysis tools (like PHPStan), helping with proper type usage and improving + code quality: + + ```php + assertType('bool', await(resolve(true))); + assertType('PromiseInterface', async(fn(): bool => true)()); + assertType('PromiseInterface', coroutine(fn(): bool => true)); + ``` + +* Feature: Full PHP 8.3 compatibility. + (#81 by @clue) + +* Update test suite to avoid unhandled promise rejections. + (#79 by @clue) + +## 4.1.0 (2023-06-22) + +* Feature: Add new `delay()` function to delay program execution. + (#69 and #78 by @clue) + + ```php + echo 'a'; + Loop::addTimer(1.0, function () { + echo 'b'; + }); + React\Async\delay(3.0); + echo 'c'; + + // prints "a" at t=0.0s + // prints "b" at t=1.0s + // prints "c" at t=3.0s + ``` + +* Update test suite, add PHPStan with `max` level and report failed assertions. + (#66 and #76 by @clue and #61 and #73 by @WyriHaximus) + +## 4.0.0 (2022-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2022/announcing-reactphp-async). + +* We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +* The v4 release will be the way forward for this package. However, we will still + actively support v3 and v2 to provide a smooth upgrade path for those not yet + on PHP 8.1+. If you're using an older PHP version, you may use either version + which all provide a compatible API but may not take advantage of newer language + features. You may target multiple versions at the same time to support a wider range of + PHP versions: + + * [`4.x` branch](https://github.com/reactphp/async/tree/4.x) (PHP 8.1+) + * [`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) + * [`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) + +This update involves some major new features and a minor BC break over the +`v3.0.0` release. We've tried hard to avoid BC breaks where possible and +minimize impact otherwise. We expect that most consumers of this package will be +affected by BC breaks, but updating should take no longer than a few minutes. +See below for more details: + +* Feature / BC break: Require PHP 8.1+ and add `mixed` type declarations. + (#14 by @clue) + +* Feature: Add Fiber-based `async()` and `await()` functions. + (#15, #18, #19 and #20 by @WyriHaximus and #26, #28, #30, #32, #34, #55 and #57 by @clue) + +* Project maintenance, rename `main` branch to `4.x` and update installation instructions. + (#29 by @clue) + +The following changes had to be ported to this release due to our branching +strategy, but also appeared in the `v3.0.0` release: + +* Feature: Support iterable type for `parallel()` + `series()` + `waterfall()`. + (#49 by @clue) + +* Feature: Forward compatibility with upcoming Promise v3. + (#48 by @clue) + +* Minor documentation improvements. + (#36 by @SimonFrings and #51 by @nhedger) + +## 3.0.0 (2022-07-11) + +See [`3.x` CHANGELOG](https://github.com/reactphp/async/blob/3.x/CHANGELOG.md) for more details. + +## 2.0.0 (2022-07-11) + +See [`2.x` CHANGELOG](https://github.com/reactphp/async/blob/2.x/CHANGELOG.md) for more details. + +## 1.0.0 (2013-02-07) + +* First tagged release diff --git a/vendor/react/async/LICENSE b/vendor/react/async/LICENSE new file mode 100644 index 0000000..44acaef --- /dev/null +++ b/vendor/react/async/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/react/async/README.md b/vendor/react/async/README.md new file mode 100644 index 0000000..9a49cde --- /dev/null +++ b/vendor/react/async/README.md @@ -0,0 +1,672 @@ +# Async Utilities + +[![CI status](https://github.com/reactphp/async/workflows/CI/badge.svg)](https://github.com/reactphp/async/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/async?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/async) + +Async utilities and fibers for [ReactPHP](https://reactphp.org/). + +This library allows you to manage async control flow. It provides a number of +combinators for [Promise](https://github.com/reactphp/promise)-based APIs. +Instead of nesting or chaining promise callbacks, you can declare them as a +list, which is resolved sequentially in an async manner. +React/Async will not automagically change blocking code to be async. You need +to have an actual event loop and non-blocking libraries interacting with that +event loop for it to work. As long as you have a Promise-based API that runs in +an event loop, it can be used with this library. + +**Table of Contents** + +* [Usage](#usage) + * [async()](#async) + * [await()](#await) + * [coroutine()](#coroutine) + * [delay()](#delay) + * [parallel()](#parallel) + * [series()](#series) + * [waterfall()](#waterfall) +* [Todo](#todo) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Usage + +This lightweight library consists only of a few simple functions. +All functions reside under the `React\Async` namespace. + +The below examples refer to all functions with their fully-qualified names like this: + +```php +React\Async\await(…); +``` + +As of PHP 5.6+ you can also import each required function into your code like this: + +```php +use function React\Async\await; + +await(…); +``` + +Alternatively, you can also use an import statement similar to this: + +```php +use React\Async; + +Async\await(…); +``` + +### async() + +The `async(callable():(PromiseInterface|T) $function): (callable():PromiseInterface)` function can be used to +return an async function for a function that uses [`await()`](#await) internally. + +This function is specifically designed to complement the [`await()` function](#await). +The [`await()` function](#await) can be considered *blocking* from the +perspective of the calling code. You can avoid this blocking behavior by +wrapping it in an `async()` function call. Everything inside this function +will still be blocked, but everything outside this function can be executed +asynchronously without blocking: + +```php +Loop::addTimer(0.5, React\Async\async(function () { + echo 'a'; + React\Async\await(React\Promise\Timer\sleep(1.0)); + echo 'c'; +})); + +Loop::addTimer(1.0, function () { + echo 'b'; +}); + +// prints "a" at t=0.5s +// prints "b" at t=1.0s +// prints "c" at t=1.5s +``` + +See also the [`await()` function](#await) for more details. + +Note that this function only works in tandem with the [`await()` function](#await). +In particular, this function does not "magically" make any blocking function +non-blocking: + +```php +Loop::addTimer(0.5, React\Async\async(function () { + echo 'a'; + sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes + echo 'c'; +})); + +Loop::addTimer(1.0, function () { + echo 'b'; +}); + +// prints "a" at t=0.5s +// prints "c" at t=1.5s: Correct timing, but wrong order +// prints "b" at t=1.5s: Triggered too late because it was blocked +``` + +As an alternative, you should always make sure to use this function in tandem +with the [`await()` function](#await) and an async API returning a promise +as shown in the previous example. + +The `async()` function is specifically designed for cases where it is used +as a callback (such as an event loop timer, event listener, or promise +callback). For this reason, it returns a new function wrapping the given +`$function` instead of directly invoking it and returning its value. + +```php +use function React\Async\async; + +Loop::addTimer(1.0, async(function () { … })); +$connection->on('close', async(function () { … })); +$stream->on('data', async(function ($data) { … })); +$promise->then(async(function (int $result) { … })); +``` + +You can invoke this wrapping function to invoke the given `$function` with +any arguments given as-is. The function will always return a Promise which +will be fulfilled with whatever your `$function` returns. Likewise, it will +return a promise that will be rejected if you throw an `Exception` or +`Throwable` from your `$function`. This allows you to easily create +Promise-based functions: + +```php +$promise = React\Async\async(function (): int { + $browser = new React\Http\Browser(); + $urls = [ + 'https://example.com/alice', + 'https://example.com/bob' + ]; + + $bytes = 0; + foreach ($urls as $url) { + $response = React\Async\await($browser->get($url)); + assert($response instanceof Psr\Http\Message\ResponseInterface); + $bytes += $response->getBody()->getSize(); + } + return $bytes; +})(); + +$promise->then(function (int $bytes) { + echo 'Total size: ' . $bytes . PHP_EOL; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +The previous example uses [`await()`](#await) inside a loop to highlight how +this vastly simplifies consuming asynchronous operations. At the same time, +this naive example does not leverage concurrent execution, as it will +essentially "await" between each operation. In order to take advantage of +concurrent execution within the given `$function`, you can "await" multiple +promises by using a single [`await()`](#await) together with Promise-based +primitives like this: + +```php +$promise = React\Async\async(function (): int { + $browser = new React\Http\Browser(); + $urls = [ + 'https://example.com/alice', + 'https://example.com/bob' + ]; + + $promises = []; + foreach ($urls as $url) { + $promises[] = $browser->get($url); + } + + try { + $responses = React\Async\await(React\Promise\all($promises)); + } catch (Exception $e) { + foreach ($promises as $promise) { + $promise->cancel(); + } + throw $e; + } + + $bytes = 0; + foreach ($responses as $response) { + assert($response instanceof Psr\Http\Message\ResponseInterface); + $bytes += $response->getBody()->getSize(); + } + return $bytes; +})(); + +$promise->then(function (int $bytes) { + echo 'Total size: ' . $bytes . PHP_EOL; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +The returned promise is implemented in such a way that it can be cancelled +when it is still pending. Cancelling a pending promise will cancel any awaited +promises inside that fiber or any nested fibers. As such, the following example +will only output `ab` and cancel the pending [`delay()`](#delay). +The [`await()`](#await) calls in this example would throw a `RuntimeException` +from the cancelled [`delay()`](#delay) call that bubbles up through the fibers. + +```php +$promise = async(static function (): int { + echo 'a'; + await(async(static function (): void { + echo 'b'; + delay(2); + echo 'c'; + })()); + echo 'd'; + + return time(); +})(); + +$promise->cancel(); +await($promise); +``` + +### await() + +The `await(PromiseInterface $promise): T` function can be used to +block waiting for the given `$promise` to be fulfilled. + +```php +$result = React\Async\await($promise); +``` + +This function will only return after the given `$promise` has settled, i.e. +either fulfilled or rejected. While the promise is pending, this function +can be considered *blocking* from the perspective of the calling code. +You can avoid this blocking behavior by wrapping it in an [`async()` function](#async) +call. Everything inside this function will still be blocked, but everything +outside this function can be executed asynchronously without blocking: + +```php +Loop::addTimer(0.5, React\Async\async(function () { + echo 'a'; + React\Async\await(React\Promise\Timer\sleep(1.0)); + echo 'c'; +})); + +Loop::addTimer(1.0, function () { + echo 'b'; +}); + +// prints "a" at t=0.5s +// prints "b" at t=1.0s +// prints "c" at t=1.5s +``` + +See also the [`async()` function](#async) for more details. + +Once the promise is fulfilled, this function will return whatever the promise +resolved to. + +Once the promise is rejected, this will throw whatever the promise rejected +with. If the promise did not reject with an `Exception` or `Throwable`, then +this function will throw an `UnexpectedValueException` instead. + +```php +try { + $result = React\Async\await($promise); + // promise successfully fulfilled with $result + echo 'Result: ' . $result; +} catch (Throwable $e) { + // promise rejected with $e + echo 'Error: ' . $e->getMessage(); +} +``` + +### coroutine() + +The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface|T) $function, mixed ...$args): PromiseInterface` function can be used to +execute a Generator-based coroutine to "await" promises. + +```php +React\Async\coroutine(function () { + $browser = new React\Http\Browser(); + + try { + $response = yield $browser->get('https://example.com/'); + assert($response instanceof Psr\Http\Message\ResponseInterface); + echo $response->getBody(); + } catch (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + } +}); +``` + +Using Generator-based coroutines is an alternative to directly using the +underlying promise APIs. For many use cases, this makes using promise-based +APIs much simpler, as it resembles a synchronous code flow more closely. +The above example performs the equivalent of directly using the promise APIs: + +```php +$browser = new React\Http\Browser(); + +$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + echo $response->getBody(); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +The `yield` keyword can be used to "await" a promise resolution. Internally, +it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php). +This allows the execution to be interrupted and resumed at the same place +when the promise is fulfilled. The `yield` statement returns whatever the +promise is fulfilled with. If the promise is rejected, it will throw an +`Exception` or `Throwable`. + +The `coroutine()` function will always return a Promise which will be +fulfilled with whatever your `$function` returns. Likewise, it will return +a promise that will be rejected if you throw an `Exception` or `Throwable` +from your `$function`. This allows you to easily create Promise-based +functions: + +```php +$promise = React\Async\coroutine(function () { + $browser = new React\Http\Browser(); + $urls = [ + 'https://example.com/alice', + 'https://example.com/bob' + ]; + + $bytes = 0; + foreach ($urls as $url) { + $response = yield $browser->get($url); + assert($response instanceof Psr\Http\Message\ResponseInterface); + $bytes += $response->getBody()->getSize(); + } + return $bytes; +}); + +$promise->then(function (int $bytes) { + echo 'Total size: ' . $bytes . PHP_EOL; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +The previous example uses a `yield` statement inside a loop to highlight how +this vastly simplifies consuming asynchronous operations. At the same time, +this naive example does not leverage concurrent execution, as it will +essentially "await" between each operation. In order to take advantage of +concurrent execution within the given `$function`, you can "await" multiple +promises by using a single `yield` together with Promise-based primitives +like this: + +```php +$promise = React\Async\coroutine(function () { + $browser = new React\Http\Browser(); + $urls = [ + 'https://example.com/alice', + 'https://example.com/bob' + ]; + + $promises = []; + foreach ($urls as $url) { + $promises[] = $browser->get($url); + } + + try { + $responses = yield React\Promise\all($promises); + } catch (Exception $e) { + foreach ($promises as $promise) { + $promise->cancel(); + } + throw $e; + } + + $bytes = 0; + foreach ($responses as $response) { + assert($response instanceof Psr\Http\Message\ResponseInterface); + $bytes += $response->getBody()->getSize(); + } + return $bytes; +}); + +$promise->then(function (int $bytes) { + echo 'Total size: ' . $bytes . PHP_EOL; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +### delay() + +The `delay(float $seconds): void` function can be used to +delay program execution for duration given in `$seconds`. + +```php +React\Async\delay($seconds); +``` + +This function will only return after the given number of `$seconds` have +elapsed. If there are no other events attached to this loop, it will behave +similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php). + +```php +echo 'a'; +React\Async\delay(1.0); +echo 'b'; + +// prints "a" at t=0.0s +// prints "b" at t=1.0s +``` + +Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php), +this function may not necessarily halt execution of the entire process thread. +Instead, it allows the event loop to run any other events attached to the +same loop until the delay returns: + +```php +echo 'a'; +Loop::addTimer(1.0, function (): void { + echo 'b'; +}); +React\Async\delay(3.0); +echo 'c'; + +// prints "a" at t=0.0s +// prints "b" at t=1.0s +// prints "c" at t=3.0s +``` + +This behavior is especially useful if you want to delay the program execution +of a particular routine, such as when building a simple polling or retry +mechanism: + +```php +try { + something(); +} catch (Throwable) { + // in case of error, retry after a short delay + React\Async\delay(1.0); + something(); +} +``` + +Because this function only returns after some time has passed, it can be +considered *blocking* from the perspective of the calling code. You can avoid +this blocking behavior by wrapping it in an [`async()` function](#async) call. +Everything inside this function will still be blocked, but everything outside +this function can be executed asynchronously without blocking: + +```php +Loop::addTimer(0.5, React\Async\async(function (): void { + echo 'a'; + React\Async\delay(1.0); + echo 'c'; +})); + +Loop::addTimer(1.0, function (): void { + echo 'b'; +}); + +// prints "a" at t=0.5s +// prints "b" at t=1.0s +// prints "c" at t=1.5s +``` + +See also the [`async()` function](#async) for more details. + +Internally, the `$seconds` argument will be used as a timer for the loop so that +it keeps running until this timer triggers. This implies that if you pass a +really small (or negative) value, it will still start a timer and will thus +trigger at the earliest possible time in the future. + +The function is implemented in such a way that it can be cancelled when it is +running inside an [`async()` function](#async). Cancelling the resulting +promise will clean up any pending timers and throw a `RuntimeException` from +the pending delay which in turn would reject the resulting promise. + +```php +$promise = async(function (): void { + echo 'a'; + delay(3.0); + echo 'b'; +})(); + +Loop::addTimer(2.0, function () use ($promise): void { + $promise->cancel(); +}); + +// prints "a" at t=0.0s +// rejects $promise at t=2.0 +// never prints "b" +``` + +### parallel() + +The `parallel(iterable> $tasks): PromiseInterface>` function can be used +like this: + +```php +then(function (array $results) { + foreach ($results as $result) { + var_dump($result); + } +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +### series() + +The `series(iterable> $tasks): PromiseInterface>` function can be used +like this: + +```php +then(function (array $results) { + foreach ($results as $result) { + var_dump($result); + } +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +### waterfall() + +The `waterfall(iterable> $tasks): PromiseInterface` function can be used +like this: + +```php +then(function ($prev) { + echo "Final result is $prev\n"; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +## Todo + + * Implement queue() + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org/). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](https://semver.org/). +This will install the latest supported version from this branch: + +```bash +composer require react/async:^4.3 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on PHP 8.1+. +It's *highly recommended to use the latest supported PHP version* for this project. + +We're committed to providing long-term support (LTS) options and to provide a +smooth upgrade path. If you're using an older PHP version, you may use the +[`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) or +[`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) which both +provide a compatible API but do not take advantage of newer language features. +You may target multiple versions at the same time to support a wider range of +PHP versions like this: + +```bash +composer require "react/async:^4 || ^3 || ^2" +``` + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +On top of this, we use PHPStan on max level to ensure type safety across the project: + +```bash +vendor/bin/phpstan +``` + +## License + +MIT, see [LICENSE file](LICENSE). + +This project is heavily influenced by [async.js](https://github.com/caolan/async). diff --git a/vendor/react/async/composer.json b/vendor/react/async/composer.json new file mode 100644 index 0000000..5d4082b --- /dev/null +++ b/vendor/react/async/composer.json @@ -0,0 +1,50 @@ +{ + "name": "react/async", + "description": "Async utilities and fibers for ReactPHP", + "keywords": ["async", "ReactPHP"], + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], + "require": { + "php": ">=8.1", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.8 || ^1.2.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39", + "phpunit/phpunit": "^9.6" + }, + "autoload": { + "psr-4": { + "React\\Async\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Async\\": "tests/" + } + } +} diff --git a/vendor/react/async/src/FiberFactory.php b/vendor/react/async/src/FiberFactory.php new file mode 100644 index 0000000..ee90818 --- /dev/null +++ b/vendor/react/async/src/FiberFactory.php @@ -0,0 +1,33 @@ + new SimpleFiber(); + } +} diff --git a/vendor/react/async/src/FiberInterface.php b/vendor/react/async/src/FiberInterface.php new file mode 100644 index 0000000..e40304e --- /dev/null +++ b/vendor/react/async/src/FiberInterface.php @@ -0,0 +1,23 @@ +> */ + private static array $map = []; + + /** + * @param \Fiber $fiber + * @param PromiseInterface $promise + */ + public static function setPromise(\Fiber $fiber, PromiseInterface $promise): void + { + self::$map[\spl_object_id($fiber)] = $promise; + } + + /** + * @param \Fiber $fiber + */ + public static function unsetPromise(\Fiber $fiber): void + { + unset(self::$map[\spl_object_id($fiber)]); + } + + /** + * @param \Fiber $fiber + * @return ?PromiseInterface + */ + public static function getPromise(\Fiber $fiber): ?PromiseInterface + { + return self::$map[\spl_object_id($fiber)] ?? null; + } +} diff --git a/vendor/react/async/src/SimpleFiber.php b/vendor/react/async/src/SimpleFiber.php new file mode 100644 index 0000000..8c5460a --- /dev/null +++ b/vendor/react/async/src/SimpleFiber.php @@ -0,0 +1,79 @@ + */ + private static ?\Fiber $scheduler = null; + + private static ?\Closure $suspend = null; + + /** @var ?\Fiber */ + private ?\Fiber $fiber = null; + + public function __construct() + { + $this->fiber = \Fiber::getCurrent(); + } + + public function resume(mixed $value): void + { + if ($this->fiber !== null) { + $this->fiber->resume($value); + } else { + self::$suspend = static fn() => $value; + } + + if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { + $suspend = self::$suspend; + self::$suspend = null; + + \Fiber::suspend($suspend); + } + } + + public function throw(\Throwable $throwable): void + { + if ($this->fiber !== null) { + $this->fiber->throw($throwable); + } else { + self::$suspend = static fn() => throw $throwable; + } + + if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { + $suspend = self::$suspend; + self::$suspend = null; + + \Fiber::suspend($suspend); + } + } + + public function suspend(): mixed + { + if ($this->fiber === null) { + if (self::$scheduler === null || self::$scheduler->isTerminated()) { + self::$scheduler = new \Fiber(static fn() => Loop::run()); + // Run event loop to completion on shutdown. + \register_shutdown_function(static function (): void { + assert(self::$scheduler instanceof \Fiber); + if (self::$scheduler->isSuspended()) { + self::$scheduler->resume(); + } + }); + } + + $ret = (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start()); + assert(\is_callable($ret)); + + return $ret(); + } + + return \Fiber::suspend(); + } +} diff --git a/vendor/react/async/src/functions.php b/vendor/react/async/src/functions.php new file mode 100644 index 0000000..bcf40c1 --- /dev/null +++ b/vendor/react/async/src/functions.php @@ -0,0 +1,846 @@ +on('close', async(function () { … })); + * $stream->on('data', async(function ($data) { … })); + * $promise->then(async(function (int $result) { … })); + * ``` + * + * You can invoke this wrapping function to invoke the given `$function` with + * any arguments given as-is. The function will always return a Promise which + * will be fulfilled with whatever your `$function` returns. Likewise, it will + * return a promise that will be rejected if you throw an `Exception` or + * `Throwable` from your `$function`. This allows you to easily create + * Promise-based functions: + * + * ```php + * $promise = React\Async\async(function (): int { + * $browser = new React\Http\Browser(); + * $urls = [ + * 'https://example.com/alice', + * 'https://example.com/bob' + * ]; + * + * $bytes = 0; + * foreach ($urls as $url) { + * $response = React\Async\await($browser->get($url)); + * assert($response instanceof Psr\Http\Message\ResponseInterface); + * $bytes += $response->getBody()->getSize(); + * } + * return $bytes; + * })(); + * + * $promise->then(function (int $bytes) { + * echo 'Total size: ' . $bytes . PHP_EOL; + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * The previous example uses [`await()`](#await) inside a loop to highlight how + * this vastly simplifies consuming asynchronous operations. At the same time, + * this naive example does not leverage concurrent execution, as it will + * essentially "await" between each operation. In order to take advantage of + * concurrent execution within the given `$function`, you can "await" multiple + * promises by using a single [`await()`](#await) together with Promise-based + * primitives like this: + * + * ```php + * $promise = React\Async\async(function (): int { + * $browser = new React\Http\Browser(); + * $urls = [ + * 'https://example.com/alice', + * 'https://example.com/bob' + * ]; + * + * $promises = []; + * foreach ($urls as $url) { + * $promises[] = $browser->get($url); + * } + * + * try { + * $responses = React\Async\await(React\Promise\all($promises)); + * } catch (Exception $e) { + * foreach ($promises as $promise) { + * $promise->cancel(); + * } + * throw $e; + * } + * + * $bytes = 0; + * foreach ($responses as $response) { + * assert($response instanceof Psr\Http\Message\ResponseInterface); + * $bytes += $response->getBody()->getSize(); + * } + * return $bytes; + * })(); + * + * $promise->then(function (int $bytes) { + * echo 'Total size: ' . $bytes . PHP_EOL; + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * The returned promise is implemented in such a way that it can be cancelled + * when it is still pending. Cancelling a pending promise will cancel any awaited + * promises inside that fiber or any nested fibers. As such, the following example + * will only output `ab` and cancel the pending [`delay()`](#delay). + * The [`await()`](#await) calls in this example would throw a `RuntimeException` + * from the cancelled [`delay()`](#delay) call that bubbles up through the fibers. + * + * ```php + * $promise = async(static function (): int { + * echo 'a'; + * await(async(static function (): void { + * echo 'b'; + * delay(2); + * echo 'c'; + * })()); + * echo 'd'; + * + * return time(); + * })(); + * + * $promise->cancel(); + * await($promise); + * ``` + * + * @template T + * @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214) + * @template A2 + * @template A3 + * @template A4 + * @template A5 + * @param callable(A1,A2,A3,A4,A5): (PromiseInterface|T) $function + * @return callable(A1=,A2=,A3=,A4=,A5=): PromiseInterface + * @since 4.0.0 + * @see coroutine() + */ +function async(callable $function): callable +{ + return static function (mixed ...$args) use ($function): PromiseInterface { + $fiber = null; + /** @var PromiseInterface $promise*/ + $promise = new Promise(function (callable $resolve, callable $reject) use ($function, $args, &$fiber): void { + $fiber = new \Fiber(function () use ($resolve, $reject, $function, $args, &$fiber): void { + try { + $resolve($function(...$args)); + } catch (\Throwable $exception) { + $reject($exception); + } finally { + assert($fiber instanceof \Fiber); + FiberMap::unsetPromise($fiber); + } + }); + + $fiber->start(); + }, function () use (&$fiber): void { + assert($fiber instanceof \Fiber); + $promise = FiberMap::getPromise($fiber); + if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { + $promise->cancel(); + } + }); + + $lowLevelFiber = \Fiber::getCurrent(); + if ($lowLevelFiber !== null) { + FiberMap::setPromise($lowLevelFiber, $promise); + } + + return $promise; + }; +} + +/** + * Block waiting for the given `$promise` to be fulfilled. + * + * ```php + * $result = React\Async\await($promise); + * ``` + * + * This function will only return after the given `$promise` has settled, i.e. + * either fulfilled or rejected. While the promise is pending, this function + * can be considered *blocking* from the perspective of the calling code. + * You can avoid this blocking behavior by wrapping it in an [`async()` function](#async) + * call. Everything inside this function will still be blocked, but everything + * outside this function can be executed asynchronously without blocking: + * + * ```php + * Loop::addTimer(0.5, React\Async\async(function () { + * echo 'a'; + * React\Async\await(React\Promise\Timer\sleep(1.0)); + * echo 'c'; + * })); + * + * Loop::addTimer(1.0, function () { + * echo 'b'; + * }); + * + * // prints "a" at t=0.5s + * // prints "b" at t=1.0s + * // prints "c" at t=1.5s + * ``` + * + * See also the [`async()` function](#async) for more details. + * + * Once the promise is fulfilled, this function will return whatever the promise + * resolved to. + * + * Once the promise is rejected, this will throw whatever the promise rejected + * with. If the promise did not reject with an `Exception` or `Throwable`, then + * this function will throw an `UnexpectedValueException` instead. + * + * ```php + * try { + * $result = React\Async\await($promise); + * // promise successfully fulfilled with $result + * echo 'Result: ' . $result; + * } catch (Throwable $e) { + * // promise rejected with $e + * echo 'Error: ' . $e->getMessage(); + * } + * ``` + * + * @template T + * @param PromiseInterface $promise + * @return T returns whatever the promise resolves to + * @throws \Exception when the promise is rejected with an `Exception` + * @throws \Throwable when the promise is rejected with a `Throwable` + * @throws \UnexpectedValueException when the promise is rejected with an unexpected value (Promise API v1 or v2 only) + */ +function await(PromiseInterface $promise): mixed +{ + $fiber = null; + $resolved = false; + $rejected = false; + + /** @var T $resolvedValue */ + $resolvedValue = null; + $rejectedThrowable = null; + $lowLevelFiber = \Fiber::getCurrent(); + + $promise->then( + function (mixed $value) use (&$resolved, &$resolvedValue, &$fiber, $lowLevelFiber): void { + if ($lowLevelFiber !== null) { + FiberMap::unsetPromise($lowLevelFiber); + } + + /** @var ?\Fiber $fiber */ + if ($fiber === null) { + $resolved = true; + /** @var T $resolvedValue */ + $resolvedValue = $value; + return; + } + + $fiber->resume($value); + }, + function (mixed $throwable) use (&$rejected, &$rejectedThrowable, &$fiber, $lowLevelFiber): void { + if ($lowLevelFiber !== null) { + FiberMap::unsetPromise($lowLevelFiber); + } + + if (!$throwable instanceof \Throwable) { + $throwable = new \UnexpectedValueException( + 'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable)) /** @phpstan-ignore-line */ + ); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $trace = $r->getValue($throwable); + assert(\is_array($trace)); + + // Exception trace arguments only available when zend.exception_ignore_args is not set + // @codeCoverageIgnoreStart + foreach ($trace as $ti => $one) { + if (isset($one['args'])) { + foreach ($one['args'] as $ai => $arg) { + if ($arg instanceof \Closure) { + $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($throwable, $trace); + } + + if ($fiber === null) { + $rejected = true; + $rejectedThrowable = $throwable; + return; + } + + $fiber->throw($throwable); + } + ); + + if ($resolved) { + return $resolvedValue; + } + + if ($rejected) { + assert($rejectedThrowable instanceof \Throwable); + throw $rejectedThrowable; + } + + if ($lowLevelFiber !== null) { + FiberMap::setPromise($lowLevelFiber, $promise); + } + + $fiber = FiberFactory::create(); + + return $fiber->suspend(); +} + +/** + * Delay program execution for duration given in `$seconds`. + * + * ```php + * React\Async\delay($seconds); + * ``` + * + * This function will only return after the given number of `$seconds` have + * elapsed. If there are no other events attached to this loop, it will behave + * similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php). + * + * ```php + * echo 'a'; + * React\Async\delay(1.0); + * echo 'b'; + * + * // prints "a" at t=0.0s + * // prints "b" at t=1.0s + * ``` + * + * Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php), + * this function may not necessarily halt execution of the entire process thread. + * Instead, it allows the event loop to run any other events attached to the + * same loop until the delay returns: + * + * ```php + * echo 'a'; + * Loop::addTimer(1.0, function (): void { + * echo 'b'; + * }); + * React\Async\delay(3.0); + * echo 'c'; + * + * // prints "a" at t=0.0s + * // prints "b" at t=1.0s + * // prints "c" at t=3.0s + * ``` + * + * This behavior is especially useful if you want to delay the program execution + * of a particular routine, such as when building a simple polling or retry + * mechanism: + * + * ```php + * try { + * something(); + * } catch (Throwable) { + * // in case of error, retry after a short delay + * React\Async\delay(1.0); + * something(); + * } + * ``` + * + * Because this function only returns after some time has passed, it can be + * considered *blocking* from the perspective of the calling code. You can avoid + * this blocking behavior by wrapping it in an [`async()` function](#async) call. + * Everything inside this function will still be blocked, but everything outside + * this function can be executed asynchronously without blocking: + * + * ```php + * Loop::addTimer(0.5, React\Async\async(function (): void { + * echo 'a'; + * React\Async\delay(1.0); + * echo 'c'; + * })); + * + * Loop::addTimer(1.0, function (): void { + * echo 'b'; + * }); + * + * // prints "a" at t=0.5s + * // prints "b" at t=1.0s + * // prints "c" at t=1.5s + * ``` + * + * See also the [`async()` function](#async) for more details. + * + * Internally, the `$seconds` argument will be used as a timer for the loop so that + * it keeps running until this timer triggers. This implies that if you pass a + * really small (or negative) value, it will still start a timer and will thus + * trigger at the earliest possible time in the future. + * + * The function is implemented in such a way that it can be cancelled when it is + * running inside an [`async()` function](#async). Cancelling the resulting + * promise will clean up any pending timers and throw a `RuntimeException` from + * the pending delay which in turn would reject the resulting promise. + * + * ```php + * $promise = async(function (): void { + * echo 'a'; + * delay(3.0); + * echo 'b'; + * })(); + * + * Loop::addTimer(2.0, function () use ($promise): void { + * $promise->cancel(); + * }); + * + * // prints "a" at t=0.0s + * // rejects $promise at t=2.0 + * // never prints "b" + * ``` + * + * @return void + * @throws \RuntimeException when the function is cancelled inside an `async()` function + * @see async() + * @uses await() + */ +function delay(float $seconds): void +{ + /** @var ?TimerInterface $timer */ + $timer = null; + + await(new Promise(function (callable $resolve) use ($seconds, &$timer): void { + $timer = Loop::addTimer($seconds, fn() => $resolve(null)); + }, function () use (&$timer): void { + assert($timer instanceof TimerInterface); + Loop::cancelTimer($timer); + throw new \RuntimeException('Delay cancelled'); + })); +} + +/** + * Execute a Generator-based coroutine to "await" promises. + * + * ```php + * React\Async\coroutine(function () { + * $browser = new React\Http\Browser(); + * + * try { + * $response = yield $browser->get('https://example.com/'); + * assert($response instanceof Psr\Http\Message\ResponseInterface); + * echo $response->getBody(); + * } catch (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * } + * }); + * ``` + * + * Using Generator-based coroutines is an alternative to directly using the + * underlying promise APIs. For many use cases, this makes using promise-based + * APIs much simpler, as it resembles a synchronous code flow more closely. + * The above example performs the equivalent of directly using the promise APIs: + * + * ```php + * $browser = new React\Http\Browser(); + * + * $browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + * echo $response->getBody(); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * The `yield` keyword can be used to "await" a promise resolution. Internally, + * it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php). + * This allows the execution to be interrupted and resumed at the same place + * when the promise is fulfilled. The `yield` statement returns whatever the + * promise is fulfilled with. If the promise is rejected, it will throw an + * `Exception` or `Throwable`. + * + * The `coroutine()` function will always return a Promise which will be + * fulfilled with whatever your `$function` returns. Likewise, it will return + * a promise that will be rejected if you throw an `Exception` or `Throwable` + * from your `$function`. This allows you to easily create Promise-based + * functions: + * + * ```php + * $promise = React\Async\coroutine(function () { + * $browser = new React\Http\Browser(); + * $urls = [ + * 'https://example.com/alice', + * 'https://example.com/bob' + * ]; + * + * $bytes = 0; + * foreach ($urls as $url) { + * $response = yield $browser->get($url); + * assert($response instanceof Psr\Http\Message\ResponseInterface); + * $bytes += $response->getBody()->getSize(); + * } + * return $bytes; + * }); + * + * $promise->then(function (int $bytes) { + * echo 'Total size: ' . $bytes . PHP_EOL; + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * The previous example uses a `yield` statement inside a loop to highlight how + * this vastly simplifies consuming asynchronous operations. At the same time, + * this naive example does not leverage concurrent execution, as it will + * essentially "await" between each operation. In order to take advantage of + * concurrent execution within the given `$function`, you can "await" multiple + * promises by using a single `yield` together with Promise-based primitives + * like this: + * + * ```php + * $promise = React\Async\coroutine(function () { + * $browser = new React\Http\Browser(); + * $urls = [ + * 'https://example.com/alice', + * 'https://example.com/bob' + * ]; + * + * $promises = []; + * foreach ($urls as $url) { + * $promises[] = $browser->get($url); + * } + * + * try { + * $responses = yield React\Promise\all($promises); + * } catch (Exception $e) { + * foreach ($promises as $promise) { + * $promise->cancel(); + * } + * throw $e; + * } + * + * $bytes = 0; + * foreach ($responses as $response) { + * assert($response instanceof Psr\Http\Message\ResponseInterface); + * $bytes += $response->getBody()->getSize(); + * } + * return $bytes; + * }); + * + * $promise->then(function (int $bytes) { + * echo 'Total size: ' . $bytes . PHP_EOL; + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * @template T + * @template TYield + * @template A1 (any number of function arguments, see https://github.com/phpstan/phpstan/issues/8214) + * @template A2 + * @template A3 + * @template A4 + * @template A5 + * @param callable(A1, A2, A3, A4, A5):(\Generator, TYield, PromiseInterface|T>|PromiseInterface|T) $function + * @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is + * @return PromiseInterface + * @since 3.0.0 + */ +function coroutine(callable $function, mixed ...$args): PromiseInterface +{ + try { + $generator = $function(...$args); + } catch (\Throwable $e) { + return reject($e); + } + + if (!$generator instanceof \Generator) { + return resolve($generator); + } + + $promise = null; + /** @var Deferred $deferred*/ + $deferred = new Deferred(function () use (&$promise) { + /** @var ?PromiseInterface $promise */ + if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { + $promise->cancel(); + } + $promise = null; + }); + + /** @var callable $next */ + $next = function () use ($deferred, $generator, &$next, &$promise) { + try { + if (!$generator->valid()) { + $next = null; + $deferred->resolve($generator->getReturn()); + return; + } + } catch (\Throwable $e) { + $next = null; + $deferred->reject($e); + return; + } + + $promise = $generator->current(); + if (!$promise instanceof PromiseInterface) { + $next = null; + $deferred->reject(new \UnexpectedValueException( + 'Expected coroutine to yield ' . PromiseInterface::class . ', but got ' . (is_object($promise) ? get_class($promise) : gettype($promise)) + )); + return; + } + + /** @var PromiseInterface $promise */ + assert($next instanceof \Closure); + $promise->then(function ($value) use ($generator, $next) { + $generator->send($value); + $next(); + }, function (\Throwable $reason) use ($generator, $next) { + $generator->throw($reason); + $next(); + })->then(null, function (\Throwable $reason) use ($deferred, &$next) { + $next = null; + $deferred->reject($reason); + }); + }; + $next(); + + return $deferred->promise(); +} + +/** + * @template T + * @param iterable|T)> $tasks + * @return PromiseInterface> + */ +function parallel(iterable $tasks): PromiseInterface +{ + /** @var array> $pending */ + $pending = []; + /** @var Deferred> $deferred */ + $deferred = new Deferred(function () use (&$pending) { + foreach ($pending as $promise) { + if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { + $promise->cancel(); + } + } + $pending = []; + }); + $results = []; + $continue = true; + + $taskErrback = function ($error) use (&$pending, $deferred, &$continue) { + $continue = false; + $deferred->reject($error); + + foreach ($pending as $promise) { + if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { + $promise->cancel(); + } + } + $pending = []; + }; + + foreach ($tasks as $i => $task) { + $taskCallback = function ($result) use (&$results, &$pending, &$continue, $i, $deferred) { + $results[$i] = $result; + unset($pending[$i]); + + if (!$pending && !$continue) { + $deferred->resolve($results); + } + }; + + $promise = \call_user_func($task); + assert($promise instanceof PromiseInterface); + $pending[$i] = $promise; + + $promise->then($taskCallback, $taskErrback); + + if (!$continue) { + break; + } + } + + $continue = false; + if (!$pending) { + $deferred->resolve($results); + } + + /** @var PromiseInterface> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */ + return $deferred->promise(); +} + +/** + * @template T + * @param iterable|T)> $tasks + * @return PromiseInterface> + */ +function series(iterable $tasks): PromiseInterface +{ + $pending = null; + /** @var Deferred> $deferred */ + $deferred = new Deferred(function () use (&$pending) { + /** @var ?PromiseInterface $pending */ + if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { + $pending->cancel(); + } + $pending = null; + }); + $results = []; + + if ($tasks instanceof \IteratorAggregate) { + $tasks = $tasks->getIterator(); + assert($tasks instanceof \Iterator); + } + + $taskCallback = function ($result) use (&$results, &$next) { + $results[] = $result; + /** @var \Closure $next */ + $next(); + }; + + $next = function () use (&$tasks, $taskCallback, $deferred, &$results, &$pending) { + if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) { + $deferred->resolve($results); + return; + } + + if ($tasks instanceof \Iterator) { + $task = $tasks->current(); + $tasks->next(); + } else { + assert(\is_array($tasks)); + $task = \array_shift($tasks); + } + + assert(\is_callable($task)); + $promise = \call_user_func($task); + assert($promise instanceof PromiseInterface); + $pending = $promise; + + $promise->then($taskCallback, array($deferred, 'reject')); + }; + + $next(); + + /** @var PromiseInterface> Remove once defining `Deferred()` above is supported by PHPStan, see https://github.com/phpstan/phpstan/issues/11032 */ + return $deferred->promise(); +} + +/** + * @template T + * @param iterable<(callable():(PromiseInterface|T))|(callable(mixed):(PromiseInterface|T))> $tasks + * @return PromiseInterface<($tasks is non-empty-array|\Traversable ? T : null)> + */ +function waterfall(iterable $tasks): PromiseInterface +{ + $pending = null; + /** @var Deferred $deferred*/ + $deferred = new Deferred(function () use (&$pending) { + /** @var ?PromiseInterface $pending */ + if ($pending instanceof PromiseInterface && \method_exists($pending, 'cancel')) { + $pending->cancel(); + } + $pending = null; + }); + + if ($tasks instanceof \IteratorAggregate) { + $tasks = $tasks->getIterator(); + assert($tasks instanceof \Iterator); + } + + /** @var callable $next */ + $next = function ($value = null) use (&$tasks, &$next, $deferred, &$pending) { + if ($tasks instanceof \Iterator ? !$tasks->valid() : !$tasks) { + $deferred->resolve($value); + return; + } + + if ($tasks instanceof \Iterator) { + $task = $tasks->current(); + $tasks->next(); + } else { + assert(\is_array($tasks)); + $task = \array_shift($tasks); + } + + assert(\is_callable($task)); + $promise = \call_user_func_array($task, func_get_args()); + assert($promise instanceof PromiseInterface); + $pending = $promise; + + $promise->then($next, array($deferred, 'reject')); + }; + + $next(); + + return $deferred->promise(); +} diff --git a/vendor/react/async/src/functions_include.php b/vendor/react/async/src/functions_include.php new file mode 100644 index 0000000..05c78fa --- /dev/null +++ b/vendor/react/async/src/functions_include.php @@ -0,0 +1,9 @@ + Contains no other changes, so it's actually fully compatible with the v0.6.0 release. + +## 0.6.0 (2019-07-04) + +* Feature / BC break: Add support for `getMultiple()`, `setMultiple()`, `deleteMultiple()`, `clear()` and `has()` + supporting multiple cache items (inspired by PSR-16). + (#32 by @krlv and #37 by @clue) + +* Documentation for TTL precision with millisecond accuracy or below and + use high-resolution timer for cache TTL on PHP 7.3+. + (#35 and #38 by @clue) + +* Improve API documentation and allow legacy HHVM to fail in Travis CI config. + (#34 and #36 by @clue) + +* Prefix all global functions calls with \ to skip the look up and resolve process and go straight to the global function. + (#31 by @WyriHaximus) + +## 0.5.0 (2018-06-25) + +* Improve documentation by describing what is expected of a class implementing `CacheInterface`. + (#21, #22, #23, #27 by @WyriHaximus) + +* Implemented (optional) Least Recently Used (LRU) cache algorithm for `ArrayCache`. + (#26 by @clue) + +* Added support for cache expiration (TTL). + (#29 by @clue and @WyriHaximus) + +* Renamed `remove` to `delete` making it more in line with `PSR-16`. + (#30 by @clue) + +## 0.4.2 (2017-12-20) + +* Improve documentation with usage and installation instructions + (#10 by @clue) + +* Improve test suite by adding PHPUnit to `require-dev` and + add forward compatibility with PHPUnit 5 and PHPUnit 6 and + sanitize Composer autoload paths + (#14 by @shaunbramley and #12 and #18 by @clue) + +## 0.4.1 (2016-02-25) + +* Repository maintenance, split off from main repo, improve test suite and documentation +* First class support for PHP7 and HHVM (#9 by @clue) +* Adjust compatibility to 5.3 (#7 by @clue) + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 + +## 0.3.2 (2013-05-10) + +* Version bump + +## 0.3.0 (2013-04-14) + +* Version bump + +## 0.2.6 (2012-12-26) + +* Feature: New cache component, used by DNS diff --git a/vendor/react/cache/LICENSE b/vendor/react/cache/LICENSE new file mode 100644 index 0000000..d6f8901 --- /dev/null +++ b/vendor/react/cache/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/react/cache/README.md b/vendor/react/cache/README.md new file mode 100644 index 0000000..7a86be9 --- /dev/null +++ b/vendor/react/cache/README.md @@ -0,0 +1,367 @@ +# Cache + +[![CI status](https://github.com/reactphp/cache/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/cache/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/cache?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/cache) + +Async, [Promise](https://github.com/reactphp/promise)-based cache interface +for [ReactPHP](https://reactphp.org/). + +The cache component provides a +[Promise](https://github.com/reactphp/promise)-based +[`CacheInterface`](#cacheinterface) and an in-memory [`ArrayCache`](#arraycache) +implementation of that. +This allows consumers to type hint against the interface and third parties to +provide alternate implementations. +This project is heavily inspired by +[PSR-16: Common Interface for Caching Libraries](https://www.php-fig.org/psr/psr-16/), +but uses an interface more suited for async, non-blocking applications. + +**Table of Contents** + +* [Usage](#usage) + * [CacheInterface](#cacheinterface) + * [get()](#get) + * [set()](#set) + * [delete()](#delete) + * [getMultiple()](#getmultiple) + * [setMultiple()](#setmultiple) + * [deleteMultiple()](#deletemultiple) + * [clear()](#clear) + * [has()](#has) + * [ArrayCache](#arraycache) +* [Common usage](#common-usage) + * [Fallback get](#fallback-get) + * [Fallback-get-and-set](#fallback-get-and-set) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Usage + +### CacheInterface + +The `CacheInterface` describes the main interface of this component. +This allows consumers to type hint against the interface and third parties to +provide alternate implementations. + +#### get() + +The `get(string $key, mixed $default = null): PromiseInterface` method can be used to +retrieve an item from the cache. + +This method will resolve with the cached value on success or with the +given `$default` value when no item can be found or when an error occurs. +Similarly, an expired cache item (once the time-to-live is expired) is +considered a cache miss. + +```php +$cache + ->get('foo') + ->then('var_dump'); +``` + +This example fetches the value of the key `foo` and passes it to the +`var_dump` function. You can use any of the composition provided by +[promises](https://github.com/reactphp/promise). + +#### set() + +The `set(string $key, mixed $value, ?float $ttl = null): PromiseInterface` method can be used to +store an item in the cache. + +This method will resolve with `true` on success or `false` when an error +occurs. If the cache implementation has to go over the network to store +it, it may take a while. + +The optional `$ttl` parameter sets the maximum time-to-live in seconds +for this cache item. If this parameter is omitted (or `null`), the item +will stay in the cache for as long as the underlying implementation +supports. Trying to access an expired cache item results in a cache miss, +see also [`get()`](#get). + +```php +$cache->set('foo', 'bar', 60); +``` + +This example eventually sets the value of the key `foo` to `bar`. If it +already exists, it is overridden. + +This interface does not enforce any particular TTL resolution, so special +care may have to be taken if you rely on very high precision with +millisecond accuracy or below. Cache implementations SHOULD work on a +best effort basis and SHOULD provide at least second accuracy unless +otherwise noted. Many existing cache implementations are known to provide +microsecond or millisecond accuracy, but it's generally not recommended +to rely on this high precision. + +This interface suggests that cache implementations SHOULD use a monotonic +time source if available. Given that a monotonic time source is only +available as of PHP 7.3 by default, cache implementations MAY fall back +to using wall-clock time. +While this does not affect many common use cases, this is an important +distinction for programs that rely on a high time precision or on systems +that are subject to discontinuous time adjustments (time jumps). +This means that if you store a cache item with a TTL of 30s and then +adjust your system time forward by 20s, the cache item SHOULD still +expire in 30s. + +#### delete() + +The `delete(string $key): PromiseInterface` method can be used to +delete an item from the cache. + +This method will resolve with `true` on success or `false` when an error +occurs. When no item for `$key` is found in the cache, it also resolves +to `true`. If the cache implementation has to go over the network to +delete it, it may take a while. + +```php +$cache->delete('foo'); +``` + +This example eventually deletes the key `foo` from the cache. As with +`set()`, this may not happen instantly and a promise is returned to +provide guarantees whether or not the item has been removed from cache. + +#### getMultiple() + +The `getMultiple(string[] $keys, mixed $default = null): PromiseInterface` method can be used to +retrieve multiple cache items by their unique keys. + +This method will resolve with an array of cached values on success or with the +given `$default` value when an item can not be found or when an error occurs. +Similarly, an expired cache item (once the time-to-live is expired) is +considered a cache miss. + +```php +$cache->getMultiple(array('name', 'age'))->then(function (array $values) { + $name = $values['name'] ?? 'User'; + $age = $values['age'] ?? 'n/a'; + + echo $name . ' is ' . $age . PHP_EOL; +}); +``` + +This example fetches the cache items for the `name` and `age` keys and +prints some example output. You can use any of the composition provided +by [promises](https://github.com/reactphp/promise). + +#### setMultiple() + +The `setMultiple(array $values, ?float $ttl = null): PromiseInterface` method can be used to +persist a set of key => value pairs in the cache, with an optional TTL. + +This method will resolve with `true` on success or `false` when an error +occurs. If the cache implementation has to go over the network to store +it, it may take a while. + +The optional `$ttl` parameter sets the maximum time-to-live in seconds +for these cache items. If this parameter is omitted (or `null`), these items +will stay in the cache for as long as the underlying implementation +supports. Trying to access an expired cache items results in a cache miss, +see also [`getMultiple()`](#getmultiple). + +```php +$cache->setMultiple(array('foo' => 1, 'bar' => 2), 60); +``` + +This example eventually sets the list of values - the key `foo` to `1` value +and the key `bar` to `2`. If some of the keys already exist, they are overridden. + +#### deleteMultiple() + +The `setMultiple(string[] $keys): PromiseInterface` method can be used to +delete multiple cache items in a single operation. + +This method will resolve with `true` on success or `false` when an error +occurs. When no items for `$keys` are found in the cache, it also resolves +to `true`. If the cache implementation has to go over the network to +delete it, it may take a while. + +```php +$cache->deleteMultiple(array('foo', 'bar, 'baz')); +``` + +This example eventually deletes keys `foo`, `bar` and `baz` from the cache. +As with `setMultiple()`, this may not happen instantly and a promise is returned to +provide guarantees whether or not the item has been removed from cache. + +#### clear() + +The `clear(): PromiseInterface` method can be used to +wipe clean the entire cache. + +This method will resolve with `true` on success or `false` when an error +occurs. If the cache implementation has to go over the network to +delete it, it may take a while. + +```php +$cache->clear(); +``` + +This example eventually deletes all keys from the cache. As with `deleteMultiple()`, +this may not happen instantly and a promise is returned to provide guarantees +whether or not all the items have been removed from cache. + +#### has() + +The `has(string $key): PromiseInterface` method can be used to +determine whether an item is present in the cache. + +This method will resolve with `true` on success or `false` when no item can be found +or when an error occurs. Similarly, an expired cache item (once the time-to-live +is expired) is considered a cache miss. + +```php +$cache + ->has('foo') + ->then('var_dump'); +``` + +This example checks if the value of the key `foo` is set in the cache and passes +the result to the `var_dump` function. You can use any of the composition provided by +[promises](https://github.com/reactphp/promise). + +NOTE: It is recommended that has() is only to be used for cache warming type purposes +and not to be used within your live applications operations for get/set, as this method +is subject to a race condition where your has() will return true and immediately after, +another script can remove it making the state of your app out of date. + +### ArrayCache + +The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). + +```php +$cache = new ArrayCache(); + +$cache->set('foo', 'bar'); +``` + +Its constructor accepts an optional `?int $limit` parameter to limit the +maximum number of entries to store in the LRU cache. If you add more +entries to this instance, it will automatically take care of removing +the one that was least recently used (LRU). + +For example, this snippet will overwrite the first value and only store +the last two entries: + +```php +$cache = new ArrayCache(2); + +$cache->set('foo', '1'); +$cache->set('bar', '2'); +$cache->set('baz', '3'); +``` + +This cache implementation is known to rely on wall-clock time to schedule +future cache expiration times when using any version before PHP 7.3, +because a monotonic time source is only available as of PHP 7.3 (`hrtime()`). +While this does not affect many common use cases, this is an important +distinction for programs that rely on a high time precision or on systems +that are subject to discontinuous time adjustments (time jumps). +This means that if you store a cache item with a TTL of 30s on PHP < 7.3 +and then adjust your system time forward by 20s, the cache item may +expire in 10s. See also [`set()`](#set) for more details. + +## Common usage + +### Fallback get + +A common use case of caches is to attempt fetching a cached value and as a +fallback retrieve it from the original data source if not found. Here is an +example of that: + +```php +$cache + ->get('foo') + ->then(function ($result) { + if ($result === null) { + return getFooFromDb(); + } + + return $result; + }) + ->then('var_dump'); +``` + +First an attempt is made to retrieve the value of `foo`. A callback function is +registered that will call `getFooFromDb` when the resulting value is null. +`getFooFromDb` is a function (can be any PHP callable) that will be called if the +key does not exist in the cache. + +`getFooFromDb` can handle the missing key by returning a promise for the +actual value from the database (or any other data source). As a result, this +chain will correctly fall back, and provide the value in both cases. + +### Fallback get and set + +To expand on the fallback get example, often you want to set the value on the +cache after fetching it from the data source. + +```php +$cache + ->get('foo') + ->then(function ($result) { + if ($result === null) { + return $this->getAndCacheFooFromDb(); + } + + return $result; + }) + ->then('var_dump'); + +public function getAndCacheFooFromDb() +{ + return $this->db + ->get('foo') + ->then(array($this, 'cacheFooFromDb')); +} + +public function cacheFooFromDb($foo) +{ + $this->cache->set('foo', $foo); + + return $foo; +} +``` + +By using chaining you can easily conditionally cache the value if it is +fetched from the database. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](https://semver.org/). +This will install the latest supported version: + +```bash +composer require react/cache:^1.2 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and +HHVM. +It's *highly recommended to use PHP 7+* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +## License + +MIT, see [LICENSE file](LICENSE). diff --git a/vendor/react/cache/composer.json b/vendor/react/cache/composer.json new file mode 100644 index 0000000..153439a --- /dev/null +++ b/vendor/react/cache/composer.json @@ -0,0 +1,45 @@ +{ + "name": "react/cache", + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": ["cache", "caching", "promise", "ReactPHP"], + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Cache\\": "tests/" + } + } +} diff --git a/vendor/react/cache/src/ArrayCache.php b/vendor/react/cache/src/ArrayCache.php new file mode 100644 index 0000000..81f25ef --- /dev/null +++ b/vendor/react/cache/src/ArrayCache.php @@ -0,0 +1,181 @@ +set('foo', 'bar'); + * ``` + * + * Its constructor accepts an optional `?int $limit` parameter to limit the + * maximum number of entries to store in the LRU cache. If you add more + * entries to this instance, it will automatically take care of removing + * the one that was least recently used (LRU). + * + * For example, this snippet will overwrite the first value and only store + * the last two entries: + * + * ```php + * $cache = new ArrayCache(2); + * + * $cache->set('foo', '1'); + * $cache->set('bar', '2'); + * $cache->set('baz', '3'); + * ``` + * + * This cache implementation is known to rely on wall-clock time to schedule + * future cache expiration times when using any version before PHP 7.3, + * because a monotonic time source is only available as of PHP 7.3 (`hrtime()`). + * While this does not affect many common use cases, this is an important + * distinction for programs that rely on a high time precision or on systems + * that are subject to discontinuous time adjustments (time jumps). + * This means that if you store a cache item with a TTL of 30s on PHP < 7.3 + * and then adjust your system time forward by 20s, the cache item may + * expire in 10s. See also [`set()`](#set) for more details. + * + * @param int|null $limit maximum number of entries to store in the LRU cache + */ + public function __construct($limit = null) + { + $this->limit = $limit; + + // prefer high-resolution timer, available as of PHP 7.3+ + $this->supportsHighResolution = \function_exists('hrtime'); + } + + public function get($key, $default = null) + { + // delete key if it is already expired => below will detect this as a cache miss + if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) { + unset($this->data[$key], $this->expires[$key]); + } + + if (!\array_key_exists($key, $this->data)) { + return Promise\resolve($default); + } + + // remove and append to end of array to keep track of LRU info + $value = $this->data[$key]; + unset($this->data[$key]); + $this->data[$key] = $value; + + return Promise\resolve($value); + } + + public function set($key, $value, $ttl = null) + { + // unset before setting to ensure this entry will be added to end of array (LRU info) + unset($this->data[$key]); + $this->data[$key] = $value; + + // sort expiration times if TTL is given (first will expire first) + unset($this->expires[$key]); + if ($ttl !== null) { + $this->expires[$key] = $this->now() + $ttl; + \asort($this->expires); + } + + // ensure size limit is not exceeded or remove first entry from array + if ($this->limit !== null && \count($this->data) > $this->limit) { + // first try to check if there's any expired entry + // expiration times are sorted, so we can simply look at the first one + \reset($this->expires); + $key = \key($this->expires); + + // check to see if the first in the list of expiring keys is already expired + // if the first key is not expired, we have to overwrite by using LRU info + if ($key === null || $this->now() - $this->expires[$key] < 0) { + \reset($this->data); + $key = \key($this->data); + } + unset($this->data[$key], $this->expires[$key]); + } + + return Promise\resolve(true); + } + + public function delete($key) + { + unset($this->data[$key], $this->expires[$key]); + + return Promise\resolve(true); + } + + public function getMultiple(array $keys, $default = null) + { + $values = array(); + + foreach ($keys as $key) { + $values[$key] = $this->get($key, $default); + } + + return Promise\all($values); + } + + public function setMultiple(array $values, $ttl = null) + { + foreach ($values as $key => $value) { + $this->set($key, $value, $ttl); + } + + return Promise\resolve(true); + } + + public function deleteMultiple(array $keys) + { + foreach ($keys as $key) { + unset($this->data[$key], $this->expires[$key]); + } + + return Promise\resolve(true); + } + + public function clear() + { + $this->data = array(); + $this->expires = array(); + + return Promise\resolve(true); + } + + public function has($key) + { + // delete key if it is already expired + if (isset($this->expires[$key]) && $this->now() - $this->expires[$key] > 0) { + unset($this->data[$key], $this->expires[$key]); + } + + if (!\array_key_exists($key, $this->data)) { + return Promise\resolve(false); + } + + // remove and append to end of array to keep track of LRU info + $value = $this->data[$key]; + unset($this->data[$key]); + $this->data[$key] = $value; + + return Promise\resolve(true); + } + + /** + * @return float + */ + private function now() + { + return $this->supportsHighResolution ? \hrtime(true) * 1e-9 : \microtime(true); + } +} diff --git a/vendor/react/cache/src/CacheInterface.php b/vendor/react/cache/src/CacheInterface.php new file mode 100644 index 0000000..8e51c19 --- /dev/null +++ b/vendor/react/cache/src/CacheInterface.php @@ -0,0 +1,194 @@ +get('foo') + * ->then('var_dump'); + * ``` + * + * This example fetches the value of the key `foo` and passes it to the + * `var_dump` function. You can use any of the composition provided by + * [promises](https://github.com/reactphp/promise). + * + * @param string $key + * @param mixed $default Default value to return for cache miss or null if not given. + * @return PromiseInterface + */ + public function get($key, $default = null); + + /** + * Stores an item in the cache. + * + * This method will resolve with `true` on success or `false` when an error + * occurs. If the cache implementation has to go over the network to store + * it, it may take a while. + * + * The optional `$ttl` parameter sets the maximum time-to-live in seconds + * for this cache item. If this parameter is omitted (or `null`), the item + * will stay in the cache for as long as the underlying implementation + * supports. Trying to access an expired cache item results in a cache miss, + * see also [`get()`](#get). + * + * ```php + * $cache->set('foo', 'bar', 60); + * ``` + * + * This example eventually sets the value of the key `foo` to `bar`. If it + * already exists, it is overridden. + * + * This interface does not enforce any particular TTL resolution, so special + * care may have to be taken if you rely on very high precision with + * millisecond accuracy or below. Cache implementations SHOULD work on a + * best effort basis and SHOULD provide at least second accuracy unless + * otherwise noted. Many existing cache implementations are known to provide + * microsecond or millisecond accuracy, but it's generally not recommended + * to rely on this high precision. + * + * This interface suggests that cache implementations SHOULD use a monotonic + * time source if available. Given that a monotonic time source is only + * available as of PHP 7.3 by default, cache implementations MAY fall back + * to using wall-clock time. + * While this does not affect many common use cases, this is an important + * distinction for programs that rely on a high time precision or on systems + * that are subject to discontinuous time adjustments (time jumps). + * This means that if you store a cache item with a TTL of 30s and then + * adjust your system time forward by 20s, the cache item SHOULD still + * expire in 30s. + * + * @param string $key + * @param mixed $value + * @param ?float $ttl + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function set($key, $value, $ttl = null); + + /** + * Deletes an item from the cache. + * + * This method will resolve with `true` on success or `false` when an error + * occurs. When no item for `$key` is found in the cache, it also resolves + * to `true`. If the cache implementation has to go over the network to + * delete it, it may take a while. + * + * ```php + * $cache->delete('foo'); + * ``` + * + * This example eventually deletes the key `foo` from the cache. As with + * `set()`, this may not happen instantly and a promise is returned to + * provide guarantees whether or not the item has been removed from cache. + * + * @param string $key + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function delete($key); + + /** + * Retrieves multiple cache items by their unique keys. + * + * This method will resolve with an array of cached values on success or with the + * given `$default` value when an item can not be found or when an error occurs. + * Similarly, an expired cache item (once the time-to-live is expired) is + * considered a cache miss. + * + * ```php + * $cache->getMultiple(array('name', 'age'))->then(function (array $values) { + * $name = $values['name'] ?? 'User'; + * $age = $values['age'] ?? 'n/a'; + * + * echo $name . ' is ' . $age . PHP_EOL; + * }); + * ``` + * + * This example fetches the cache items for the `name` and `age` keys and + * prints some example output. You can use any of the composition provided + * by [promises](https://github.com/reactphp/promise). + * + * @param string[] $keys A list of keys that can obtained in a single operation. + * @param mixed $default Default value to return for keys that do not exist. + * @return PromiseInterface Returns a promise which resolves to an `array` of cached values + */ + public function getMultiple(array $keys, $default = null); + + /** + * Persists a set of key => value pairs in the cache, with an optional TTL. + * + * This method will resolve with `true` on success or `false` when an error + * occurs. If the cache implementation has to go over the network to store + * it, it may take a while. + * + * The optional `$ttl` parameter sets the maximum time-to-live in seconds + * for these cache items. If this parameter is omitted (or `null`), these items + * will stay in the cache for as long as the underlying implementation + * supports. Trying to access an expired cache items results in a cache miss, + * see also [`get()`](#get). + * + * ```php + * $cache->setMultiple(array('foo' => 1, 'bar' => 2), 60); + * ``` + * + * This example eventually sets the list of values - the key `foo` to 1 value + * and the key `bar` to 2. If some of the keys already exist, they are overridden. + * + * @param array $values A list of key => value pairs for a multiple-set operation. + * @param ?float $ttl Optional. The TTL value of this item. + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function setMultiple(array $values, $ttl = null); + + /** + * Deletes multiple cache items in a single operation. + * + * @param string[] $keys A list of string-based keys to be deleted. + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function deleteMultiple(array $keys); + + /** + * Wipes clean the entire cache. + * + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function clear(); + + /** + * Determines whether an item is present in the cache. + * + * This method will resolve with `true` on success or `false` when no item can be found + * or when an error occurs. Similarly, an expired cache item (once the time-to-live + * is expired) is considered a cache miss. + * + * ```php + * $cache + * ->has('foo') + * ->then('var_dump'); + * ``` + * + * This example checks if the value of the key `foo` is set in the cache and passes + * the result to the `var_dump` function. You can use any of the composition provided by + * [promises](https://github.com/reactphp/promise). + * + * NOTE: It is recommended that has() is only to be used for cache warming type purposes + * and not to be used within your live applications operations for get/set, as this method + * is subject to a race condition where your has() will return true and immediately after, + * another script can remove it making the state of your app out of date. + * + * @param string $key The cache item key. + * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error + */ + public function has($key); +} diff --git a/vendor/react/dns/CHANGELOG.md b/vendor/react/dns/CHANGELOG.md new file mode 100644 index 0000000..bc1055f --- /dev/null +++ b/vendor/react/dns/CHANGELOG.md @@ -0,0 +1,452 @@ +# Changelog + +## 1.13.0 (2024-06-13) + +* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations. + (#224 by @WyriHaximus) + +## 1.12.0 (2023-11-29) + +* Feature: Full PHP 8.3 compatibility. + (#217 by @sergiy-petrov) + +* Update test environment and avoid unhandled promise rejections. + (#215, #216 and #218 by @clue) + +## 1.11.0 (2023-06-02) + +* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer. + (#213 by @clue) + +* Improve test suite and project setup and report failed assertions. + (#210 by @clue, #212 by @WyriHaximus and #209 and #211 by @SimonFrings) + +## 1.10.0 (2022-09-08) + +* Feature: Full support for PHP 8.2 release. + (#201 by @clue and #207 by @WyriHaximus) + +* Feature: Optimize forward compatibility with Promise v3, avoid hitting autoloader. + (#202 by @clue) + +* Feature / Fix: Improve error reporting when custom error handler is used. + (#197 by @clue) + +* Fix: Fix invalid references in exception stack trace. + (#191 by @clue) + +* Minor documentation improvements. + (#195 by @SimonFrings and #203 by @nhedger) + +* Improve test suite, update to use default loop and new reactphp/async package. + (#204, #205 and #206 by @clue and #196 by @SimonFrings) + +## 1.9.0 (2021-12-20) + +* Feature: Full support for PHP 8.1 release and prepare PHP 8.2 compatibility + by refactoring `Parser` to avoid assigning dynamic properties. + (#188 and #186 by @clue and #184 by @SimonFrings) + +* Feature: Avoid dependency on `ext-filter`. + (#185 by @clue) + +* Feature / Fix: Skip invalid nameserver entries from `resolv.conf` and ignore IPv6 zone IDs. + (#187 by @clue) + +* Feature / Fix: Reduce socket read chunk size for queries over TCP/IP. + (#189 by @clue) + +## 1.8.0 (2021-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). + (#182 by @clue) + + ```php + // old (still supported) + $factory = new React\Dns\Resolver\Factory(); + $resolver = $factory->create($config, $loop); + + // new (using default loop) + $factory = new React\Dns\Resolver\Factory(); + $resolver = $factory->create($config); + ``` + +## 1.7.0 (2021-06-25) + +* Feature: Update DNS `Factory` to accept complete `Config` object. + Add new `FallbackExecutor` and use fallback DNS servers when `Config` lists multiple servers. + (#179 and #180 by @clue) + + ```php + // old (still supported) + $config = React\Dns\Config\Config::loadSystemConfigBlocking(); + $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + $resolver = $factory->create($server, $loop); + + // new + $config = React\Dns\Config\Config::loadSystemConfigBlocking(); + if (!$config->nameservers) { + $config->nameservers[] = '8.8.8.8'; + } + $resolver = $factory->create($config, $loop); + ``` + +## 1.6.0 (2021-06-21) + +* Feature: Add support for legacy `SPF` record type. + (#178 by @akondas and @clue) + +* Fix: Fix integer overflow for TCP/IP chunk size on 32 bit platforms. + (#177 by @clue) + +## 1.5.0 (2021-03-05) + +* Feature: Improve error reporting when query fails, include domain and query type and DNS server address where applicable. + (#174 by @clue) + +* Feature: Improve error handling when sending data to DNS server fails (macOS). + (#171 and #172 by @clue) + +* Fix: Improve DNS response parser to limit recursion for compressed labels. + (#169 by @clue) + +* Improve test suite, use GitHub actions for continuous integration (CI). + (#170 by @SimonFrings) + +## 1.4.0 (2020-09-18) + +* Feature: Support upcoming PHP 8. + (#168 by @clue) + +* Improve test suite and update to PHPUnit 9.3. + (#164 by @clue, #165 and #166 by @SimonFrings and #167 by @WyriHaximus) + +## 1.3.0 (2020-07-10) + +* Feature: Forward compatibility with react/promise v3. + (#153 by @WyriHaximus) + +* Feature: Support parsing `OPT` records (EDNS0). + (#157 by @clue) + +* Fix: Avoid PHP warnings due to lack of args in exception trace on PHP 7.4. + (#160 by @clue) + +* Improve test suite and add `.gitattributes` to exclude dev files from exports. + Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite. + (#154 by @reedy, #156 by @clue and #163 by @SimonFrings) + +## 1.2.0 (2019-08-15) + +* Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection, + add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and + automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`. + (#145, #146, #147 and #148 by @clue) + +* Feature: Support escaping literal dots and special characters in domain names. + (#144 by @clue) + +## 1.1.0 (2019-07-18) + +* Feature: Support parsing `CAA` and `SSHFP` records. + (#141 and #142 by @clue) + +* Feature: Add `ResolverInterface` as common interface for `Resolver` class. + (#139 by @clue) + +* Fix: Add missing private property definitions and + remove unneeded dependency on `react/stream`. + (#140 and #143 by @clue) + +## 1.0.0 (2019-07-11) + +* First stable LTS release, now following [SemVer](https://semver.org/). + We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +This update involves a number of BC breaks due to dropped support for +deprecated functionality and some internal API cleanup. We've tried hard to +avoid BC breaks where possible and minimize impact otherwise. We expect that +most consumers of this package will actually not be affected by any BC +breaks, see below for more details: + +* BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions + instead of nested arrays and increase code coverage to 100%. + (#130 by @clue) + +* BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`, + remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and + add API documentation for `ExecutorInterface`. + (#135, #137 and #138 by @clue) + +* BC break: Replace `HeaderBag` attributes with simple `Message` properties. + (#132 by @clue) + +* BC break: Mark all `Record` attributes as required, add documentation vs `Query`. + (#136 by @clue) + +* BC break: Mark all classes as final to discourage inheritance + (#134 by @WyriHaximus) + +## 0.4.19 (2019-07-10) + +* Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6. + (#133 by @clue) + +## 0.4.18 (2019-09-07) + +* Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`, + respect TTL from response records when caching and do not cache truncated responses. + (#129 by @clue) + +* Feature: Limit cache size to 256 last responses by default. + (#127 by @clue) + +* Feature: Cooperatively resolve hosts to avoid running same query concurrently. + (#125 by @clue) + +## 0.4.17 (2019-04-01) + +* Feature: Support parsing `authority` and `additional` records from DNS response. + (#123 by @clue) + +* Feature: Support dumping records as part of outgoing binary DNS message. + (#124 by @clue) + +* Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0 + (#121 by @clue) + +* Improve test suite to add forward compatibility with PHPUnit 7, + test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM. + (#122 by @clue) + +## 0.4.16 (2018-11-11) + +* Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references. + (#118 by @clue) + +* Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages, + malformed record data or malformed compressed domain name labels. + (#115 and #117 by @clue) + +* Fix: Fix interpretation of TTL as UINT32 with most significant bit unset. + (#116 by @clue) + +* Fix: Fix caching advanced MX/SRV/TXT/SOA structures. + (#112 by @clue) + +## 0.4.15 (2018-07-02) + +* Feature: Add `resolveAll()` method to support custom query types in `Resolver`. + (#110 by @clue and @WyriHaximus) + + ```php + $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { + echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; + }); + ``` + +* Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records. + (#104, #105, #106, #107 and #108 by @clue) + +* Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data. + (#104 by @clue) + +* Feature: Improve error messages for failed queries and improve documentation. + (#109 by @clue) + +* Feature: Add reverse DNS lookup example. + (#111 by @clue) + +## 0.4.14 (2018-06-26) + +* Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages + to avoid cache poisoning attacks and deprecate legacy `Executor`. + (#101 and #103 by @clue) + +* Feature: Forward compatibility with Cache 0.5 + (#102 by @clue) + +* Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API. + (#99 by @clue) + +## 0.4.13 (2018-02-27) + +* Add `Config::loadSystemConfigBlocking()` to load default system config + and support parsing DNS config on all supported platforms + (`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows) + (#92, #93, #94 and #95 by @clue) + + ```php + $config = Config::loadSystemConfigBlocking(); + $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; + ``` + +* Remove unneeded cyclic dependency on react/socket + (#96 by @clue) + +## 0.4.12 (2018-01-14) + +* Improve test suite by adding forward compatibility with PHPUnit 6, + test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases, + add test group to skip integration tests relying on internet connection + and add minor documentation improvements. + (#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor) + +## 0.4.11 (2017-08-25) + +* Feature: Support resolving from default hosts file + (#75, #76 and #77 by @clue) + + This means that resolving hosts such as `localhost` will now work as + expected across all platforms with no changes required: + + ```php + $resolver->resolve('localhost')->then(function ($ip) { + echo 'IP: ' . $ip; + }); + ``` + + The new `HostsExecutor` exists for advanced usage and is otherwise used + internally for this feature. + +## 0.4.10 (2017-08-10) + +* Feature: Forward compatibility with EventLoop v1.0 and v0.5 and + lock minimum dependencies and work around circular dependency for tests + (#70 and #71 by @clue) + +* Fix: Work around DNS timeout issues for Windows users + (#74 by @clue) + +* Documentation and examples for advanced usage + (#66 by @WyriHaximus) + +* Remove broken TCP code, do not retry with invalid TCP query + (#73 by @clue) + +* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and + lock Travis distro so new defaults will not break the build and + fix failing tests for PHP 7.1 + (#68 by @WyriHaximus and #69 and #72 by @clue) + +## 0.4.9 (2017-05-01) + +* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 + (#61 by @clue) + +## 0.4.8 (2017-04-16) + +* Feature: Add support for the AAAA record type to the protocol parser + (#58 by @othillo) + +* Feature: Add support for the PTR record type to the protocol parser + (#59 by @othillo) + +## 0.4.7 (2017-03-31) + +* Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component + (#57 by @clue) + +## 0.4.6 (2017-03-11) + +* Fix: Fix DNS timeout issues for Windows users and add forward compatibility + with Stream v0.5 and upcoming v0.6 + (#53 by @clue) + +* Improve test suite by adding PHPUnit to `require-dev` + (#54 by @clue) + +## 0.4.5 (2017-03-02) + +* Fix: Ensure we ignore the case of the answer + (#51 by @WyriHaximus) + +* Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal + code re-use for upcoming versions. + (#48 and #49 by @clue) + +## 0.4.4 (2017-02-13) + +* Fix: Fix handling connection and stream errors + (#45 by @clue) + +* Feature: Add examples and forward compatibility with upcoming Socket v0.5 component + (#46 and #47 by @clue) + +## 0.4.3 (2016-07-31) + +* Feature: Allow for cache adapter injection (#38 by @WyriHaximus) + + ```php + $factory = new React\Dns\Resolver\Factory(); + + $cache = new MyCustomCacheInstance(); + $resolver = $factory->createCached('8.8.8.8', $loop, $cache); + ``` + +* Feature: Support Promise cancellation (#35 by @clue) + + ```php + $promise = $resolver->resolve('reactphp.org'); + + $promise->cancel(); + ``` + +## 0.4.2 (2016-02-24) + +* Repository maintenance, split off from main repo, improve test suite and documentation +* First class support for PHP7 and HHVM (#34 by @clue) +* Adjust compatibility to 5.3 (#30 by @clue) + +## 0.4.1 (2014-04-13) + +* Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus) + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* Bug fix: Properly resolve CNAME aliases +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 +* Bump React dependencies to v0.4 + +## 0.3.2 (2013-05-10) + +* Feature: Support default port for IPv6 addresses (@clue) + +## 0.3.0 (2013-04-14) + +* Bump React dependencies to v0.3 + +## 0.2.6 (2012-12-26) + +* Feature: New cache component, used by DNS + +## 0.2.5 (2012-11-26) + +* Version bump + +## 0.2.4 (2012-11-18) + +* Feature: Change to promise-based API (@jsor) + +## 0.2.3 (2012-11-14) + +* Version bump + +## 0.2.2 (2012-10-28) + +* Feature: DNS executor timeout handling (@arnaud-lb) +* Feature: DNS retry executor (@arnaud-lb) + +## 0.2.1 (2012-10-14) + +* Minor adjustments to DNS parser + +## 0.2.0 (2012-09-10) + +* Feature: DNS resolver diff --git a/vendor/react/dns/LICENSE b/vendor/react/dns/LICENSE new file mode 100644 index 0000000..d6f8901 --- /dev/null +++ b/vendor/react/dns/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/react/dns/README.md b/vendor/react/dns/README.md new file mode 100644 index 0000000..9f83a94 --- /dev/null +++ b/vendor/react/dns/README.md @@ -0,0 +1,453 @@ +# DNS + +[![CI status](https://github.com/reactphp/dns/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/dns/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/dns?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/dns) + +Async DNS resolver for [ReactPHP](https://reactphp.org/). + +The main point of the DNS component is to provide async DNS resolution. +However, it is really a toolkit for working with DNS messages, and could +easily be used to create a DNS server. + +**Table of contents** + +* [Basic usage](#basic-usage) +* [Caching](#caching) + * [Custom cache adapter](#custom-cache-adapter) +* [ResolverInterface](#resolverinterface) + * [resolve()](#resolve) + * [resolveAll()](#resolveall) +* [Advanced usage](#advanced-usage) + * [UdpTransportExecutor](#udptransportexecutor) + * [TcpTransportExecutor](#tcptransportexecutor) + * [SelectiveTransportExecutor](#selectivetransportexecutor) + * [HostsFileExecutor](#hostsfileexecutor) +* [Install](#install) +* [Tests](#tests) +* [License](#license) +* [References](#references) + +## Basic usage + +The most basic usage is to just create a resolver through the resolver +factory. All you need to give it is a nameserver, then you can start resolving +names, baby! + +```php +$config = React\Dns\Config\Config::loadSystemConfigBlocking(); +if (!$config->nameservers) { + $config->nameservers[] = '8.8.8.8'; +} + +$factory = new React\Dns\Resolver\Factory(); +$dns = $factory->create($config); + +$dns->resolve('igor.io')->then(function ($ip) { + echo "Host: $ip\n"; +}); +``` + +See also the [first example](examples). + +The `Config` class can be used to load the system default config. This is an +operation that may access the filesystem and block. Ideally, this method should +thus be executed only once before the loop starts and not repeatedly while it is +running. +Note that this class may return an *empty* configuration if the system config +can not be loaded. As such, you'll likely want to apply a default nameserver +as above if none can be found. + +> Note that the factory loads the hosts file from the filesystem once when + creating the resolver instance. + Ideally, this method should thus be executed only once before the loop starts + and not repeatedly while it is running. + +But there's more. + +## Caching + +You can cache results by configuring the resolver to use a `CachedExecutor`: + +```php +$config = React\Dns\Config\Config::loadSystemConfigBlocking(); +if (!$config->nameservers) { + $config->nameservers[] = '8.8.8.8'; +} + +$factory = new React\Dns\Resolver\Factory(); +$dns = $factory->createCached($config); + +$dns->resolve('igor.io')->then(function ($ip) { + echo "Host: $ip\n"; +}); + +... + +$dns->resolve('igor.io')->then(function ($ip) { + echo "Host: $ip\n"; +}); +``` + +If the first call returns before the second, only one query will be executed. +The second result will be served from an in memory cache. +This is particularly useful for long running scripts where the same hostnames +have to be looked up multiple times. + +See also the [third example](examples). + +### Custom cache adapter + +By default, the above will use an in memory cache. + +You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead: + +```php +$cache = new React\Cache\ArrayCache(); +$factory = new React\Dns\Resolver\Factory(); +$dns = $factory->createCached('8.8.8.8', null, $cache); +``` + +See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). + +## ResolverInterface + + + +### resolve() + +The `resolve(string $domain): PromiseInterface` method can be used to +resolve the given $domain name to a single IPv4 address (type `A` query). + +```php +$resolver->resolve('reactphp.org')->then(function ($ip) { + echo 'IP for reactphp.org is ' . $ip . PHP_EOL; +}); +``` + +This is one of the main methods in this package. It sends a DNS query +for the given $domain name to your DNS server and returns a single IP +address on success. + +If the DNS server sends a DNS response message that contains more than +one IP address for this query, it will randomly pick one of the IP +addresses from the response. If you want the full list of IP addresses +or want to send a different type of query, you should use the +[`resolveAll()`](#resolveall) method instead. + +If the DNS server sends a DNS response message that indicates an error +code, this method will reject with a `RecordNotFoundException`. Its +message and code can be used to check for the response code. + +If the DNS communication fails and the server does not respond with a +valid response message, this message will reject with an `Exception`. + +Pending DNS queries can be cancelled by cancelling its pending promise like so: + +```php +$promise = $resolver->resolve('reactphp.org'); + +$promise->cancel(); +``` + +### resolveAll() + +The `resolveAll(string $host, int $type): PromiseInterface` method can be used to +resolve all record values for the given $domain name and query $type. + +```php +$resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { + echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; +}); + +$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { + echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; +}); +``` + +This is one of the main methods in this package. It sends a DNS query +for the given $domain name to your DNS server and returns a list with all +record values on success. + +If the DNS server sends a DNS response message that contains one or more +records for this query, it will return a list with all record values +from the response. You can use the `Message::TYPE_*` constants to control +which type of query will be sent. Note that this method always returns a +list of record values, but each record value type depends on the query +type. For example, it returns the IPv4 addresses for type `A` queries, +the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, +`CNAME` and `PTR` queries and structured data for other queries. See also +the `Record` documentation for more details. + +If the DNS server sends a DNS response message that indicates an error +code, this method will reject with a `RecordNotFoundException`. Its +message and code can be used to check for the response code. + +If the DNS communication fails and the server does not respond with a +valid response message, this message will reject with an `Exception`. + +Pending DNS queries can be cancelled by cancelling its pending promise like so: + +```php +$promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); + +$promise->cancel(); +``` + +## Advanced Usage + +### UdpTransportExecutor + +The `UdpTransportExecutor` can be used to +send DNS queries over a UDP transport. + +This is the main class that sends a DNS query to your DNS server and is used +internally by the `Resolver` for the actual message transport. + +For more advanced usages one can utilize this class directly. +The following example looks up the `IPv6` address for `igor.io`. + +```php +$executor = new UdpTransportExecutor('8.8.8.8:53'); + +$executor->query( + new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) +)->then(function (Message $message) { + foreach ($message->answers as $answer) { + echo 'IPv6: ' . $answer->data . PHP_EOL; + } +}, 'printf'); +``` + +See also the [fourth example](examples). + +Note that this executor does not implement a timeout, so you will very likely +want to use this in combination with a `TimeoutExecutor` like this: + +```php +$executor = new TimeoutExecutor( + new UdpTransportExecutor($nameserver), + 3.0 +); +``` + +Also note that this executor uses an unreliable UDP transport and that it +does not implement any retry logic, so you will likely want to use this in +combination with a `RetryExecutor` like this: + +```php +$executor = new RetryExecutor( + new TimeoutExecutor( + new UdpTransportExecutor($nameserver), + 3.0 + ) +); +``` + +Note that this executor is entirely async and as such allows you to execute +any number of queries concurrently. You should probably limit the number of +concurrent queries in your application or you're very likely going to face +rate limitations and bans on the resolver end. For many common applications, +you may want to avoid sending the same query multiple times when the first +one is still pending, so you will likely want to use this in combination with +a `CoopExecutor` like this: + +```php +$executor = new CoopExecutor( + new RetryExecutor( + new TimeoutExecutor( + new UdpTransportExecutor($nameserver), + 3.0 + ) + ) +); +``` + +> Internally, this class uses PHP's UDP sockets and does not take advantage + of [react/datagram](https://github.com/reactphp/datagram) purely for + organizational reasons to avoid a cyclic dependency between the two + packages. Higher-level components should take advantage of the Datagram + component instead of reimplementing this socket logic from scratch. + +### TcpTransportExecutor + +The `TcpTransportExecutor` class can be used to +send DNS queries over a TCP/IP stream transport. + +This is one of the main classes that send a DNS query to your DNS server. + +For more advanced usages one can utilize this class directly. +The following example looks up the `IPv6` address for `reactphp.org`. + +```php +$executor = new TcpTransportExecutor('8.8.8.8:53'); + +$executor->query( + new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) +)->then(function (Message $message) { + foreach ($message->answers as $answer) { + echo 'IPv6: ' . $answer->data . PHP_EOL; + } +}, 'printf'); +``` + +See also [example #92](examples). + +Note that this executor does not implement a timeout, so you will very likely +want to use this in combination with a `TimeoutExecutor` like this: + +```php +$executor = new TimeoutExecutor( + new TcpTransportExecutor($nameserver), + 3.0 +); +``` + +Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP +transport, so you do not necessarily have to implement any retry logic. + +Note that this executor is entirely async and as such allows you to execute +queries concurrently. The first query will establish a TCP/IP socket +connection to the DNS server which will be kept open for a short period. +Additional queries will automatically reuse this existing socket connection +to the DNS server, will pipeline multiple requests over this single +connection and will keep an idle connection open for a short period. The +initial TCP/IP connection overhead may incur a slight delay if you only send +occasional queries – when sending a larger number of concurrent queries over +an existing connection, it becomes increasingly more efficient and avoids +creating many concurrent sockets like the UDP-based executor. You may still +want to limit the number of (concurrent) queries in your application or you +may be facing rate limitations and bans on the resolver end. For many common +applications, you may want to avoid sending the same query multiple times +when the first one is still pending, so you will likely want to use this in +combination with a `CoopExecutor` like this: + +```php +$executor = new CoopExecutor( + new TimeoutExecutor( + new TcpTransportExecutor($nameserver), + 3.0 + ) +); +``` + +> Internally, this class uses PHP's TCP/IP sockets and does not take advantage + of [react/socket](https://github.com/reactphp/socket) purely for + organizational reasons to avoid a cyclic dependency between the two + packages. Higher-level components should take advantage of the Socket + component instead of reimplementing this socket logic from scratch. + +### SelectiveTransportExecutor + +The `SelectiveTransportExecutor` class can be used to +Send DNS queries over a UDP or TCP/IP stream transport. + +This class will automatically choose the correct transport protocol to send +a DNS query to your DNS server. It will always try to send it over the more +efficient UDP transport first. If this query yields a size related issue +(truncated messages), it will retry over a streaming TCP/IP transport. + +For more advanced usages one can utilize this class directly. +The following example looks up the `IPv6` address for `reactphp.org`. + +```php +$executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor); + +$executor->query( + new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) +)->then(function (Message $message) { + foreach ($message->answers as $answer) { + echo 'IPv6: ' . $answer->data . PHP_EOL; + } +}, 'printf'); +``` + +Note that this executor only implements the logic to select the correct +transport for the given DNS query. Implementing the correct transport logic, +implementing timeouts and any retry logic is left up to the given executors, +see also [`UdpTransportExecutor`](#udptransportexecutor) and +[`TcpTransportExecutor`](#tcptransportexecutor) for more details. + +Note that this executor is entirely async and as such allows you to execute +any number of queries concurrently. You should probably limit the number of +concurrent queries in your application or you're very likely going to face +rate limitations and bans on the resolver end. For many common applications, +you may want to avoid sending the same query multiple times when the first +one is still pending, so you will likely want to use this in combination with +a `CoopExecutor` like this: + +```php +$executor = new CoopExecutor( + new SelectiveTransportExecutor( + $datagramExecutor, + $streamExecutor + ) +); +``` + +### HostsFileExecutor + +Note that the above `UdpTransportExecutor` class always performs an actual DNS query. +If you also want to take entries from your hosts file into account, you may +use this code: + +```php +$hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); + +$executor = new UdpTransportExecutor('8.8.8.8:53'); +$executor = new HostsFileExecutor($hosts, $executor); + +$executor->query( + new Query('localhost', Message::TYPE_A, Message::CLASS_IN) +); +``` + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org/). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](https://semver.org/). +This will install the latest supported version: + +```bash +composer require react/dns:^1.13 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and +HHVM. +It's *highly recommended to use the latest supported PHP version* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +vendor/bin/phpunit --exclude-group internet +``` + +## License + +MIT, see [LICENSE file](LICENSE). + +## References + +* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities +* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification diff --git a/vendor/react/dns/composer.json b/vendor/react/dns/composer.json new file mode 100644 index 0000000..4fe5c0d --- /dev/null +++ b/vendor/react/dns/composer.json @@ -0,0 +1,49 @@ +{ + "name": "react/dns", + "description": "Async DNS resolver for ReactPHP", + "keywords": ["dns", "dns-resolver", "ReactPHP", "async"], + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Dns\\": "tests/" + } + } +} diff --git a/vendor/react/dns/src/BadServerException.php b/vendor/react/dns/src/BadServerException.php new file mode 100644 index 0000000..3d95213 --- /dev/null +++ b/vendor/react/dns/src/BadServerException.php @@ -0,0 +1,7 @@ + `fe80:1`) + if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) { + $ip = substr($ip, 0, $pos); + } + + if (@inet_pton($ip) !== false) { + $config->nameservers[] = $ip; + } + } + + return $config; + } + + /** + * Loads the DNS configurations from Windows's WMIC (from the given command or default command) + * + * Note that this method blocks while loading the given command and should + * thus be used with care! While this should be relatively fast for normal + * WMIC commands, it remains unknown if this may block under certain + * circumstances. In particular, this method should only be executed before + * the loop starts, not while it is running. + * + * Note that this method will only try to execute the given command try to + * parse its output, irrespective of whether this command exists. In + * particular, this command is only available on Windows. Currently, this + * will only parse valid nameserver entries from the command output and will + * ignore all other output without complaining. + * + * Note that the previous section implies that this may return an empty + * `Config` object if no valid nameserver entries can be found. + * + * @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing + * @return self + * @link https://ss64.com/nt/wmic.html + */ + public static function loadWmicBlocking($command = null) + { + $contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command); + preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches); + + $config = new self(); + $config->nameservers = $matches[1]; + + return $config; + } + + public $nameservers = array(); +} diff --git a/vendor/react/dns/src/Config/HostsFile.php b/vendor/react/dns/src/Config/HostsFile.php new file mode 100644 index 0000000..1060231 --- /dev/null +++ b/vendor/react/dns/src/Config/HostsFile.php @@ -0,0 +1,153 @@ +contents = $contents; + } + + /** + * Returns all IPs for the given hostname + * + * @param string $name + * @return string[] + */ + public function getIpsForHost($name) + { + $name = strtolower($name); + + $ips = array(); + foreach (preg_split('/\r?\n/', $this->contents) as $line) { + $parts = preg_split('/\s+/', $line); + $ip = array_shift($parts); + if ($parts && array_search($name, $parts) !== false) { + // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) + if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) { + $ip = substr($ip, 0, $pos); + } + + if (@inet_pton($ip) !== false) { + $ips[] = $ip; + } + } + } + + return $ips; + } + + /** + * Returns all hostnames for the given IPv4 or IPv6 address + * + * @param string $ip + * @return string[] + */ + public function getHostsForIp($ip) + { + // check binary representation of IP to avoid string case and short notation + $ip = @inet_pton($ip); + if ($ip === false) { + return array(); + } + + $names = array(); + foreach (preg_split('/\r?\n/', $this->contents) as $line) { + $parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY); + $addr = (string) array_shift($parts); + + // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) + if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) { + $addr = substr($addr, 0, $pos); + } + + if (@inet_pton($addr) === $ip) { + foreach ($parts as $part) { + $names[] = $part; + } + } + } + + return $names; + } +} diff --git a/vendor/react/dns/src/Model/Message.php b/vendor/react/dns/src/Model/Message.php new file mode 100644 index 0000000..bac2b10 --- /dev/null +++ b/vendor/react/dns/src/Model/Message.php @@ -0,0 +1,230 @@ +id = self::generateId(); + $request->rd = true; + $request->questions[] = $query; + + return $request; + } + + /** + * Creates a new response message for the given query with the given answer records + * + * @param Query $query + * @param Record[] $answers + * @return self + */ + public static function createResponseWithAnswersForQuery(Query $query, array $answers) + { + $response = new Message(); + $response->id = self::generateId(); + $response->qr = true; + $response->rd = true; + + $response->questions[] = $query; + + foreach ($answers as $record) { + $response->answers[] = $record; + } + + return $response; + } + + /** + * generates a random 16 bit message ID + * + * This uses a CSPRNG so that an outside attacker that is sending spoofed + * DNS response messages can not guess the message ID to avoid possible + * cache poisoning attacks. + * + * The `random_int()` function is only available on PHP 7+ or when + * https://github.com/paragonie/random_compat is installed. As such, using + * the latest supported PHP version is highly recommended. This currently + * falls back to a less secure random number generator on older PHP versions + * in the hope that this system is properly protected against outside + * attackers, for example by using one of the common local DNS proxy stubs. + * + * @return int + * @see self::getId() + * @codeCoverageIgnore + */ + private static function generateId() + { + if (function_exists('random_int')) { + return random_int(0, 0xffff); + } + return mt_rand(0, 0xffff); + } + + /** + * The 16 bit message ID + * + * The response message ID has to match the request message ID. This allows + * the receiver to verify this is the correct response message. An outside + * attacker may try to inject fake responses by "guessing" the message ID, + * so this should use a proper CSPRNG to avoid possible cache poisoning. + * + * @var int 16 bit message ID + * @see self::generateId() + */ + public $id = 0; + + /** + * @var bool Query/Response flag, query=false or response=true + */ + public $qr = false; + + /** + * @var int specifies the kind of query (4 bit), see self::OPCODE_* constants + * @see self::OPCODE_QUERY + */ + public $opcode = self::OPCODE_QUERY; + + /** + * + * @var bool Authoritative Answer + */ + public $aa = false; + + /** + * @var bool TrunCation + */ + public $tc = false; + + /** + * @var bool Recursion Desired + */ + public $rd = false; + + /** + * @var bool Recursion Available + */ + public $ra = false; + + /** + * @var int response code (4 bit), see self::RCODE_* constants + * @see self::RCODE_OK + */ + public $rcode = Message::RCODE_OK; + + /** + * An array of Query objects + * + * ```php + * $questions = array( + * new Query( + * 'reactphp.org', + * Message::TYPE_A, + * Message::CLASS_IN + * ) + * ); + * ``` + * + * @var Query[] + */ + public $questions = array(); + + /** + * @var Record[] + */ + public $answers = array(); + + /** + * @var Record[] + */ + public $authority = array(); + + /** + * @var Record[] + */ + public $additional = array(); +} diff --git a/vendor/react/dns/src/Model/Record.php b/vendor/react/dns/src/Model/Record.php new file mode 100644 index 0000000..c20403f --- /dev/null +++ b/vendor/react/dns/src/Model/Record.php @@ -0,0 +1,153 @@ +name = $name; + $this->type = $type; + $this->class = $class; + $this->ttl = $ttl; + $this->data = $data; + } +} diff --git a/vendor/react/dns/src/Protocol/BinaryDumper.php b/vendor/react/dns/src/Protocol/BinaryDumper.php new file mode 100644 index 0000000..6f4030f --- /dev/null +++ b/vendor/react/dns/src/Protocol/BinaryDumper.php @@ -0,0 +1,199 @@ +headerToBinary($message); + $data .= $this->questionToBinary($message->questions); + $data .= $this->recordsToBinary($message->answers); + $data .= $this->recordsToBinary($message->authority); + $data .= $this->recordsToBinary($message->additional); + + return $data; + } + + /** + * @param Message $message + * @return string + */ + private function headerToBinary(Message $message) + { + $data = ''; + + $data .= pack('n', $message->id); + + $flags = 0x00; + $flags = ($flags << 1) | ($message->qr ? 1 : 0); + $flags = ($flags << 4) | $message->opcode; + $flags = ($flags << 1) | ($message->aa ? 1 : 0); + $flags = ($flags << 1) | ($message->tc ? 1 : 0); + $flags = ($flags << 1) | ($message->rd ? 1 : 0); + $flags = ($flags << 1) | ($message->ra ? 1 : 0); + $flags = ($flags << 3) | 0; // skip unused zero bit + $flags = ($flags << 4) | $message->rcode; + + $data .= pack('n', $flags); + + $data .= pack('n', count($message->questions)); + $data .= pack('n', count($message->answers)); + $data .= pack('n', count($message->authority)); + $data .= pack('n', count($message->additional)); + + return $data; + } + + /** + * @param Query[] $questions + * @return string + */ + private function questionToBinary(array $questions) + { + $data = ''; + + foreach ($questions as $question) { + $data .= $this->domainNameToBinary($question->name); + $data .= pack('n*', $question->type, $question->class); + } + + return $data; + } + + /** + * @param Record[] $records + * @return string + */ + private function recordsToBinary(array $records) + { + $data = ''; + + foreach ($records as $record) { + /* @var $record Record */ + switch ($record->type) { + case Message::TYPE_A: + case Message::TYPE_AAAA: + $binary = \inet_pton($record->data); + break; + case Message::TYPE_CNAME: + case Message::TYPE_NS: + case Message::TYPE_PTR: + $binary = $this->domainNameToBinary($record->data); + break; + case Message::TYPE_TXT: + case Message::TYPE_SPF: + $binary = $this->textsToBinary($record->data); + break; + case Message::TYPE_MX: + $binary = \pack( + 'n', + $record->data['priority'] + ); + $binary .= $this->domainNameToBinary($record->data['target']); + break; + case Message::TYPE_SRV: + $binary = \pack( + 'n*', + $record->data['priority'], + $record->data['weight'], + $record->data['port'] + ); + $binary .= $this->domainNameToBinary($record->data['target']); + break; + case Message::TYPE_SOA: + $binary = $this->domainNameToBinary($record->data['mname']); + $binary .= $this->domainNameToBinary($record->data['rname']); + $binary .= \pack( + 'N*', + $record->data['serial'], + $record->data['refresh'], + $record->data['retry'], + $record->data['expire'], + $record->data['minimum'] + ); + break; + case Message::TYPE_CAA: + $binary = \pack( + 'C*', + $record->data['flag'], + \strlen($record->data['tag']) + ); + $binary .= $record->data['tag']; + $binary .= $record->data['value']; + break; + case Message::TYPE_SSHFP: + $binary = \pack( + 'CCH*', + $record->data['algorithm'], + $record->data['type'], + $record->data['fingerprint'] + ); + break; + case Message::TYPE_OPT: + $binary = ''; + foreach ($record->data as $opt => $value) { + if ($opt === Message::OPT_TCP_KEEPALIVE && $value !== null) { + $value = \pack('n', round($value * 10)); + } + $binary .= \pack('n*', $opt, \strlen((string) $value)) . $value; + } + break; + default: + // RDATA is already stored as binary value for unknown record types + $binary = $record->data; + } + + $data .= $this->domainNameToBinary($record->name); + $data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary)); + $data .= $binary; + } + + return $data; + } + + /** + * @param string[] $texts + * @return string + */ + private function textsToBinary(array $texts) + { + $data = ''; + foreach ($texts as $text) { + $data .= \chr(\strlen($text)) . $text; + } + return $data; + } + + /** + * @param string $host + * @return string + */ + private function domainNameToBinary($host) + { + if ($host === '') { + return "\0"; + } + + // break up domain name at each dot that is not preceeded by a backslash (escaped notation) + return $this->textsToBinary( + \array_map( + 'stripcslashes', + \preg_split( + '/(?parse($data, 0); + if ($message === null) { + throw new InvalidArgumentException('Unable to parse binary message'); + } + + return $message; + } + + /** + * @param string $data + * @param int $consumed + * @return ?Message + */ + private function parse($data, $consumed) + { + if (!isset($data[12 - 1])) { + return null; + } + + list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($data, 0, 12))); + + $message = new Message(); + $message->id = $id; + $message->rcode = $fields & 0xf; + $message->ra = (($fields >> 7) & 1) === 1; + $message->rd = (($fields >> 8) & 1) === 1; + $message->tc = (($fields >> 9) & 1) === 1; + $message->aa = (($fields >> 10) & 1) === 1; + $message->opcode = ($fields >> 11) & 0xf; + $message->qr = (($fields >> 15) & 1) === 1; + $consumed += 12; + + // parse all questions + for ($i = $qdCount; $i > 0; --$i) { + list($question, $consumed) = $this->parseQuestion($data, $consumed); + if ($question === null) { + return null; + } else { + $message->questions[] = $question; + } + } + + // parse all answer records + for ($i = $anCount; $i > 0; --$i) { + list($record, $consumed) = $this->parseRecord($data, $consumed); + if ($record === null) { + return null; + } else { + $message->answers[] = $record; + } + } + + // parse all authority records + for ($i = $nsCount; $i > 0; --$i) { + list($record, $consumed) = $this->parseRecord($data, $consumed); + if ($record === null) { + return null; + } else { + $message->authority[] = $record; + } + } + + // parse all additional records + for ($i = $arCount; $i > 0; --$i) { + list($record, $consumed) = $this->parseRecord($data, $consumed); + if ($record === null) { + return null; + } else { + $message->additional[] = $record; + } + } + + return $message; + } + + /** + * @param string $data + * @param int $consumed + * @return array + */ + private function parseQuestion($data, $consumed) + { + list($labels, $consumed) = $this->readLabels($data, $consumed); + + if ($labels === null || !isset($data[$consumed + 4 - 1])) { + return array(null, null); + } + + list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4))); + $consumed += 4; + + return array( + new Query( + implode('.', $labels), + $type, + $class + ), + $consumed + ); + } + + /** + * @param string $data + * @param int $consumed + * @return array An array with a parsed Record on success or array with null if data is invalid/incomplete + */ + private function parseRecord($data, $consumed) + { + list($name, $consumed) = $this->readDomain($data, $consumed); + + if ($name === null || !isset($data[$consumed + 10 - 1])) { + return array(null, null); + } + + list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4))); + $consumed += 4; + + list($ttl) = array_values(unpack('N', substr($data, $consumed, 4))); + $consumed += 4; + + // TTL is a UINT32 that must not have most significant bit set for BC reasons + if ($ttl < 0 || $ttl >= 1 << 31) { + $ttl = 0; + } + + list($rdLength) = array_values(unpack('n', substr($data, $consumed, 2))); + $consumed += 2; + + if (!isset($data[$consumed + $rdLength - 1])) { + return array(null, null); + } + + $rdata = null; + $expected = $consumed + $rdLength; + + if (Message::TYPE_A === $type) { + if ($rdLength === 4) { + $rdata = inet_ntop(substr($data, $consumed, $rdLength)); + $consumed += $rdLength; + } + } elseif (Message::TYPE_AAAA === $type) { + if ($rdLength === 16) { + $rdata = inet_ntop(substr($data, $consumed, $rdLength)); + $consumed += $rdLength; + } + } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) { + list($rdata, $consumed) = $this->readDomain($data, $consumed); + } elseif (Message::TYPE_TXT === $type || Message::TYPE_SPF === $type) { + $rdata = array(); + while ($consumed < $expected) { + $len = ord($data[$consumed]); + $rdata[] = (string)substr($data, $consumed + 1, $len); + $consumed += $len + 1; + } + } elseif (Message::TYPE_MX === $type) { + if ($rdLength > 2) { + list($priority) = array_values(unpack('n', substr($data, $consumed, 2))); + list($target, $consumed) = $this->readDomain($data, $consumed + 2); + + $rdata = array( + 'priority' => $priority, + 'target' => $target + ); + } + } elseif (Message::TYPE_SRV === $type) { + if ($rdLength > 6) { + list($priority, $weight, $port) = array_values(unpack('n*', substr($data, $consumed, 6))); + list($target, $consumed) = $this->readDomain($data, $consumed + 6); + + $rdata = array( + 'priority' => $priority, + 'weight' => $weight, + 'port' => $port, + 'target' => $target + ); + } + } elseif (Message::TYPE_SSHFP === $type) { + if ($rdLength > 2) { + list($algorithm, $hash) = \array_values(\unpack('C*', \substr($data, $consumed, 2))); + $fingerprint = \bin2hex(\substr($data, $consumed + 2, $rdLength - 2)); + $consumed += $rdLength; + + $rdata = array( + 'algorithm' => $algorithm, + 'type' => $hash, + 'fingerprint' => $fingerprint + ); + } + } elseif (Message::TYPE_SOA === $type) { + list($mname, $consumed) = $this->readDomain($data, $consumed); + list($rname, $consumed) = $this->readDomain($data, $consumed); + + if ($mname !== null && $rname !== null && isset($data[$consumed + 20 - 1])) { + list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($data, $consumed, 20))); + $consumed += 20; + + $rdata = array( + 'mname' => $mname, + 'rname' => $rname, + 'serial' => $serial, + 'refresh' => $refresh, + 'retry' => $retry, + 'expire' => $expire, + 'minimum' => $minimum + ); + } + } elseif (Message::TYPE_OPT === $type) { + $rdata = array(); + while (isset($data[$consumed + 4 - 1])) { + list($code, $length) = array_values(unpack('n*', substr($data, $consumed, 4))); + $value = (string) substr($data, $consumed + 4, $length); + if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') { + $value = null; + } elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) { + list($value) = array_values(unpack('n', $value)); + $value = round($value * 0.1, 1); + } elseif ($code === Message::OPT_TCP_KEEPALIVE) { + break; + } + $rdata[$code] = $value; + $consumed += 4 + $length; + } + } elseif (Message::TYPE_CAA === $type) { + if ($rdLength > 3) { + list($flag, $tagLength) = array_values(unpack('C*', substr($data, $consumed, 2))); + + if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) { + $tag = substr($data, $consumed + 2, $tagLength); + $value = substr($data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength); + $consumed += $rdLength; + + $rdata = array( + 'flag' => $flag, + 'tag' => $tag, + 'value' => $value + ); + } + } + } else { + // unknown types simply parse rdata as an opaque binary string + $rdata = substr($data, $consumed, $rdLength); + $consumed += $rdLength; + } + + // ensure parsing record data consumes expact number of bytes indicated in record length + if ($consumed !== $expected || $rdata === null) { + return array(null, null); + } + + return array( + new Record($name, $type, $class, $ttl, $rdata), + $consumed + ); + } + + private function readDomain($data, $consumed) + { + list ($labels, $consumed) = $this->readLabels($data, $consumed); + + if ($labels === null) { + return array(null, null); + } + + // use escaped notation for each label part, then join using dots + return array( + \implode( + '.', + \array_map( + function ($label) { + return \addcslashes($label, "\0..\40.\177"); + }, + $labels + ) + ), + $consumed + ); + } + + /** + * @param string $data + * @param int $consumed + * @param int $compressionDepth maximum depth for compressed labels to avoid unreasonable recursion + * @return array + */ + private function readLabels($data, $consumed, $compressionDepth = 127) + { + $labels = array(); + + while (true) { + if (!isset($data[$consumed])) { + return array(null, null); + } + + $length = \ord($data[$consumed]); + + // end of labels reached + if ($length === 0) { + $consumed += 1; + break; + } + + // first two bits set? this is a compressed label (14 bit pointer offset) + if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1]) && $compressionDepth) { + $offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]); + if ($offset >= $consumed) { + return array(null, null); + } + + $consumed += 2; + list($newLabels) = $this->readLabels($data, $offset, $compressionDepth - 1); + + if ($newLabels === null) { + return array(null, null); + } + + $labels = array_merge($labels, $newLabels); + break; + } + + // length MUST be 0-63 (6 bits only) and data has to be large enough + if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) { + return array(null, null); + } + + $labels[] = substr($data, $consumed + 1, $length); + $consumed += $length + 1; + } + + return array($labels, $consumed); + } +} diff --git a/vendor/react/dns/src/Query/CachingExecutor.php b/vendor/react/dns/src/Query/CachingExecutor.php new file mode 100644 index 0000000..e530b24 --- /dev/null +++ b/vendor/react/dns/src/Query/CachingExecutor.php @@ -0,0 +1,88 @@ +executor = $executor; + $this->cache = $cache; + } + + public function query(Query $query) + { + $id = $query->name . ':' . $query->type . ':' . $query->class; + $cache = $this->cache; + $that = $this; + $executor = $this->executor; + + $pending = $cache->get($id); + return new Promise(function ($resolve, $reject) use ($query, $id, $cache, $executor, &$pending, $that) { + $pending->then( + function ($message) use ($query, $id, $cache, $executor, &$pending, $that) { + // return cached response message on cache hit + if ($message !== null) { + return $message; + } + + // perform DNS lookup if not already cached + return $pending = $executor->query($query)->then( + function (Message $message) use ($cache, $id, $that) { + // DNS response message received => store in cache when not truncated and return + if (!$message->tc) { + $cache->set($id, $message, $that->ttl($message)); + } + + return $message; + } + ); + } + )->then($resolve, function ($e) use ($reject, &$pending) { + $reject($e); + $pending = null; + }); + }, function ($_, $reject) use (&$pending, $query) { + $reject(new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled')); + $pending->cancel(); + $pending = null; + }); + } + + /** + * @param Message $message + * @return int + * @internal + */ + public function ttl(Message $message) + { + // select TTL from answers (should all be the same), use smallest value if available + // @link https://tools.ietf.org/html/rfc2181#section-5.2 + $ttl = null; + foreach ($message->answers as $answer) { + if ($ttl === null || $answer->ttl < $ttl) { + $ttl = $answer->ttl; + } + } + + if ($ttl === null) { + $ttl = self::TTL; + } + + return $ttl; + } +} diff --git a/vendor/react/dns/src/Query/CancellationException.php b/vendor/react/dns/src/Query/CancellationException.php new file mode 100644 index 0000000..5432b36 --- /dev/null +++ b/vendor/react/dns/src/Query/CancellationException.php @@ -0,0 +1,7 @@ +executor = $base; + } + + public function query(Query $query) + { + $key = $this->serializeQueryToIdentity($query); + if (isset($this->pending[$key])) { + // same query is already pending, so use shared reference to pending query + $promise = $this->pending[$key]; + ++$this->counts[$key]; + } else { + // no such query pending, so start new query and keep reference until it's fulfilled or rejected + $promise = $this->executor->query($query); + $this->pending[$key] = $promise; + $this->counts[$key] = 1; + + $pending =& $this->pending; + $counts =& $this->counts; + $promise->then(function () use ($key, &$pending, &$counts) { + unset($pending[$key], $counts[$key]); + }, function () use ($key, &$pending, &$counts) { + unset($pending[$key], $counts[$key]); + }); + } + + // Return a child promise awaiting the pending query. + // Cancelling this child promise should only cancel the pending query + // when no other child promise is awaiting the same query. + $pending =& $this->pending; + $counts =& $this->counts; + return new Promise(function ($resolve, $reject) use ($promise) { + $promise->then($resolve, $reject); + }, function () use (&$promise, $key, $query, &$pending, &$counts) { + if (--$counts[$key] < 1) { + unset($pending[$key], $counts[$key]); + $promise->cancel(); + $promise = null; + } + throw new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled'); + }); + } + + private function serializeQueryToIdentity(Query $query) + { + return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); + } +} diff --git a/vendor/react/dns/src/Query/ExecutorInterface.php b/vendor/react/dns/src/Query/ExecutorInterface.php new file mode 100644 index 0000000..0bc3945 --- /dev/null +++ b/vendor/react/dns/src/Query/ExecutorInterface.php @@ -0,0 +1,43 @@ +query($query)->then( + * function (React\Dns\Model\Message $response) { + * // response message successfully received + * var_dump($response->rcode, $response->answers); + * }, + * function (Exception $error) { + * // failed to query due to $error + * } + * ); + * ``` + * + * The returned Promise MUST be implemented in such a way that it can be + * cancelled when it is still pending. Cancelling a pending promise MUST + * reject its value with an Exception. It SHOULD clean up any underlying + * resources and references as applicable. + * + * ```php + * $promise = $executor->query($query); + * + * $promise->cancel(); + * ``` + * + * @param Query $query + * @return \React\Promise\PromiseInterface<\React\Dns\Model\Message> + * resolves with response message on success or rejects with an Exception on error + */ + public function query(Query $query); +} diff --git a/vendor/react/dns/src/Query/FallbackExecutor.php b/vendor/react/dns/src/Query/FallbackExecutor.php new file mode 100644 index 0000000..83bd360 --- /dev/null +++ b/vendor/react/dns/src/Query/FallbackExecutor.php @@ -0,0 +1,49 @@ +executor = $executor; + $this->fallback = $fallback; + } + + public function query(Query $query) + { + $cancelled = false; + $fallback = $this->fallback; + $promise = $this->executor->query($query); + + return new Promise(function ($resolve, $reject) use (&$promise, $fallback, $query, &$cancelled) { + $promise->then($resolve, function (\Exception $e1) use ($fallback, $query, $resolve, $reject, &$cancelled, &$promise) { + // reject if primary resolution rejected due to cancellation + if ($cancelled) { + $reject($e1); + return; + } + + // start fallback query if primary query rejected + $promise = $fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) { + $append = $e2->getMessage(); + if (($pos = strpos($append, ':')) !== false) { + $append = substr($append, $pos + 2); + } + + // reject with combined error message if both queries fail + $reject(new \RuntimeException($e1->getMessage() . '. ' . $append)); + }); + }); + }, function () use (&$promise, &$cancelled) { + // cancel pending query (primary or fallback) + $cancelled = true; + $promise->cancel(); + }); + } +} diff --git a/vendor/react/dns/src/Query/HostsFileExecutor.php b/vendor/react/dns/src/Query/HostsFileExecutor.php new file mode 100644 index 0000000..d6e2d93 --- /dev/null +++ b/vendor/react/dns/src/Query/HostsFileExecutor.php @@ -0,0 +1,89 @@ +hosts = $hosts; + $this->fallback = $fallback; + } + + public function query(Query $query) + { + if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) { + // forward lookup for type A or AAAA + $records = array(); + $expectsColon = $query->type === Message::TYPE_AAAA; + foreach ($this->hosts->getIpsForHost($query->name) as $ip) { + // ensure this is an IPv4/IPV6 address according to query type + if ((strpos($ip, ':') !== false) === $expectsColon) { + $records[] = new Record($query->name, $query->type, $query->class, 0, $ip); + } + } + + if ($records) { + return Promise\resolve( + Message::createResponseWithAnswersForQuery($query, $records) + ); + } + } elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) { + // reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain + $ip = $this->getIpFromHost($query->name); + + if ($ip !== null) { + $records = array(); + foreach ($this->hosts->getHostsForIp($ip) as $host) { + $records[] = new Record($query->name, $query->type, $query->class, 0, $host); + } + + if ($records) { + return Promise\resolve( + Message::createResponseWithAnswersForQuery($query, $records) + ); + } + } + } + + return $this->fallback->query($query); + } + + private function getIpFromHost($host) + { + if (substr($host, -13) === '.in-addr.arpa') { + // IPv4: read as IP and reverse bytes + $ip = @inet_pton(substr($host, 0, -13)); + if ($ip === false || isset($ip[4])) { + return null; + } + + return inet_ntop(strrev($ip)); + } elseif (substr($host, -9) === '.ip6.arpa') { + // IPv6: replace dots, reverse nibbles and interpret as hexadecimal string + $ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9))))); + if ($ip === false) { + return null; + } + + return $ip; + } else { + return null; + } + } +} diff --git a/vendor/react/dns/src/Query/Query.php b/vendor/react/dns/src/Query/Query.php new file mode 100644 index 0000000..a3dcfb5 --- /dev/null +++ b/vendor/react/dns/src/Query/Query.php @@ -0,0 +1,69 @@ +name = $name; + $this->type = $type; + $this->class = $class; + } + + /** + * Describes the hostname and query type/class for this query + * + * The output format is supposed to be human readable and is subject to change. + * The format is inspired by RFC 3597 when handling unkown types/classes. + * + * @return string "example.com (A)" or "example.com (CLASS0 TYPE1234)" + * @link https://tools.ietf.org/html/rfc3597 + */ + public function describe() + { + $class = $this->class !== Message::CLASS_IN ? 'CLASS' . $this->class . ' ' : ''; + + $type = 'TYPE' . $this->type; + $ref = new \ReflectionClass('React\Dns\Model\Message'); + foreach ($ref->getConstants() as $name => $value) { + if ($value === $this->type && \strpos($name, 'TYPE_') === 0) { + $type = \substr($name, 5); + break; + } + } + + return $this->name . ' (' . $class . $type . ')'; + } +} diff --git a/vendor/react/dns/src/Query/RetryExecutor.php b/vendor/react/dns/src/Query/RetryExecutor.php new file mode 100644 index 0000000..880609b --- /dev/null +++ b/vendor/react/dns/src/Query/RetryExecutor.php @@ -0,0 +1,85 @@ +executor = $executor; + $this->retries = $retries; + } + + public function query(Query $query) + { + return $this->tryQuery($query, $this->retries); + } + + public function tryQuery(Query $query, $retries) + { + $deferred = new Deferred(function () use (&$promise) { + if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { + $promise->cancel(); + } + }); + + $success = function ($value) use ($deferred, &$errorback) { + $errorback = null; + $deferred->resolve($value); + }; + + $executor = $this->executor; + $errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries, $executor) { + if (!$e instanceof TimeoutException) { + $errorback = null; + $deferred->reject($e); + } elseif ($retries <= 0) { + $errorback = null; + $deferred->reject($e = new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: too many retries', + 0, + $e + )); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $r->setAccessible(true); + $trace = $r->getValue($e); + + // Exception trace arguments are not available on some PHP 7.4 installs + // @codeCoverageIgnoreStart + foreach ($trace as $ti => $one) { + if (isset($one['args'])) { + foreach ($one['args'] as $ai => $arg) { + if ($arg instanceof \Closure) { + $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($e, $trace); + } else { + --$retries; + $promise = $executor->query($query)->then( + $success, + $errorback + ); + } + }; + + $promise = $this->executor->query($query)->then( + $success, + $errorback + ); + + return $deferred->promise(); + } +} diff --git a/vendor/react/dns/src/Query/SelectiveTransportExecutor.php b/vendor/react/dns/src/Query/SelectiveTransportExecutor.php new file mode 100644 index 0000000..0f0ca5d --- /dev/null +++ b/vendor/react/dns/src/Query/SelectiveTransportExecutor.php @@ -0,0 +1,85 @@ +query( + * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) + * )->then(function (Message $message) { + * foreach ($message->answers as $answer) { + * echo 'IPv6: ' . $answer->data . PHP_EOL; + * } + * }, 'printf'); + * ``` + * + * Note that this executor only implements the logic to select the correct + * transport for the given DNS query. Implementing the correct transport logic, + * implementing timeouts and any retry logic is left up to the given executors, + * see also [`UdpTransportExecutor`](#udptransportexecutor) and + * [`TcpTransportExecutor`](#tcptransportexecutor) for more details. + * + * Note that this executor is entirely async and as such allows you to execute + * any number of queries concurrently. You should probably limit the number of + * concurrent queries in your application or you're very likely going to face + * rate limitations and bans on the resolver end. For many common applications, + * you may want to avoid sending the same query multiple times when the first + * one is still pending, so you will likely want to use this in combination with + * a `CoopExecutor` like this: + * + * ```php + * $executor = new CoopExecutor( + * new SelectiveTransportExecutor( + * $datagramExecutor, + * $streamExecutor + * ) + * ); + * ``` + */ +class SelectiveTransportExecutor implements ExecutorInterface +{ + private $datagramExecutor; + private $streamExecutor; + + public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor) + { + $this->datagramExecutor = $datagramExecutor; + $this->streamExecutor = $streamExecutor; + } + + public function query(Query $query) + { + $stream = $this->streamExecutor; + $pending = $this->datagramExecutor->query($query); + + return new Promise(function ($resolve, $reject) use (&$pending, $stream, $query) { + $pending->then( + $resolve, + function ($e) use (&$pending, $stream, $query, $resolve, $reject) { + if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) { + $pending = $stream->query($query)->then($resolve, $reject); + } else { + $reject($e); + } + } + ); + }, function () use (&$pending) { + $pending->cancel(); + $pending = null; + }); + } +} diff --git a/vendor/react/dns/src/Query/TcpTransportExecutor.php b/vendor/react/dns/src/Query/TcpTransportExecutor.php new file mode 100644 index 0000000..669fd01 --- /dev/null +++ b/vendor/react/dns/src/Query/TcpTransportExecutor.php @@ -0,0 +1,382 @@ +query( + * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) + * )->then(function (Message $message) { + * foreach ($message->answers as $answer) { + * echo 'IPv6: ' . $answer->data . PHP_EOL; + * } + * }, 'printf'); + * ``` + * + * See also [example #92](examples). + * + * Note that this executor does not implement a timeout, so you will very likely + * want to use this in combination with a `TimeoutExecutor` like this: + * + * ```php + * $executor = new TimeoutExecutor( + * new TcpTransportExecutor($nameserver), + * 3.0 + * ); + * ``` + * + * Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP + * transport, so you do not necessarily have to implement any retry logic. + * + * Note that this executor is entirely async and as such allows you to execute + * queries concurrently. The first query will establish a TCP/IP socket + * connection to the DNS server which will be kept open for a short period. + * Additional queries will automatically reuse this existing socket connection + * to the DNS server, will pipeline multiple requests over this single + * connection and will keep an idle connection open for a short period. The + * initial TCP/IP connection overhead may incur a slight delay if you only send + * occasional queries – when sending a larger number of concurrent queries over + * an existing connection, it becomes increasingly more efficient and avoids + * creating many concurrent sockets like the UDP-based executor. You may still + * want to limit the number of (concurrent) queries in your application or you + * may be facing rate limitations and bans on the resolver end. For many common + * applications, you may want to avoid sending the same query multiple times + * when the first one is still pending, so you will likely want to use this in + * combination with a `CoopExecutor` like this: + * + * ```php + * $executor = new CoopExecutor( + * new TimeoutExecutor( + * new TcpTransportExecutor($nameserver), + * 3.0 + * ) + * ); + * ``` + * + * > Internally, this class uses PHP's TCP/IP sockets and does not take advantage + * of [react/socket](https://github.com/reactphp/socket) purely for + * organizational reasons to avoid a cyclic dependency between the two + * packages. Higher-level components should take advantage of the Socket + * component instead of reimplementing this socket logic from scratch. + */ +class TcpTransportExecutor implements ExecutorInterface +{ + private $nameserver; + private $loop; + private $parser; + private $dumper; + + /** + * @var ?resource + */ + private $socket; + + /** + * @var Deferred[] + */ + private $pending = array(); + + /** + * @var string[] + */ + private $names = array(); + + /** + * Maximum idle time when socket is current unused (i.e. no pending queries outstanding) + * + * If a new query is to be sent during the idle period, we can reuse the + * existing socket without having to wait for a new socket connection. + * This uses a rather small, hard-coded value to not keep any unneeded + * sockets open and to not keep the loop busy longer than needed. + * + * A future implementation may take advantage of `edns-tcp-keepalive` to keep + * the socket open for longer periods. This will likely require explicit + * configuration because this may consume additional resources and also keep + * the loop busy for longer than expected in some applications. + * + * @var float + * @link https://tools.ietf.org/html/rfc7766#section-6.2.1 + * @link https://tools.ietf.org/html/rfc7828 + */ + private $idlePeriod = 0.001; + + /** + * @var ?\React\EventLoop\TimerInterface + */ + private $idleTimer; + + private $writeBuffer = ''; + private $writePending = false; + + private $readBuffer = ''; + private $readPending = false; + + /** @var string */ + private $readChunk = 0xffff; + + /** + * @param string $nameserver + * @param ?LoopInterface $loop + */ + public function __construct($nameserver, $loop = null) + { + if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) { + // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets + $nameserver = '[' . $nameserver . ']'; + } + + $parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver); + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) { + throw new \InvalidArgumentException('Invalid nameserver address given'); + } + + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); + } + + $this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); + $this->loop = $loop ?: Loop::get(); + $this->parser = new Parser(); + $this->dumper = new BinaryDumper(); + } + + public function query(Query $query) + { + $request = Message::createRequestForQuery($query); + + // keep shuffing message ID to avoid using the same message ID for two pending queries at the same time + while (isset($this->pending[$request->id])) { + $request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore + } + + $queryData = $this->dumper->toBinary($request); + $length = \strlen($queryData); + if ($length > 0xffff) { + return \React\Promise\reject(new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport' + )); + } + + $queryData = \pack('n', $length) . $queryData; + + if ($this->socket === null) { + // create async TCP/IP connection (may take a while) + $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT); + if ($socket === false) { + return \React\Promise\reject(new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', + $errno + )); + } + + // set socket to non-blocking and wait for it to become writable (connection success/rejected) + \stream_set_blocking($socket, false); + if (\function_exists('stream_set_chunk_size')) { + \stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore + } + $this->socket = $socket; + } + + if ($this->idleTimer !== null) { + $this->loop->cancelTimer($this->idleTimer); + $this->idleTimer = null; + } + + // wait for socket to become writable to actually write out data + $this->writeBuffer .= $queryData; + if (!$this->writePending) { + $this->writePending = true; + $this->loop->addWriteStream($this->socket, array($this, 'handleWritable')); + } + + $names =& $this->names; + $that = $this; + $deferred = new Deferred(function () use ($that, &$names, $request) { + // remove from list of pending names, but remember pending query + $name = $names[$request->id]; + unset($names[$request->id]); + $that->checkIdle(); + + throw new CancellationException('DNS query for ' . $name . ' has been cancelled'); + }); + + $this->pending[$request->id] = $deferred; + $this->names[$request->id] = $query->describe(); + + return $deferred->promise(); + } + + /** + * @internal + */ + public function handleWritable() + { + if ($this->readPending === false) { + $name = @\stream_socket_get_name($this->socket, true); + if ($name === false) { + // Connection failed? Check socket error if available for underlying errno/errstr. + // @codeCoverageIgnoreStart + if (\function_exists('socket_import_stream')) { + $socket = \socket_import_stream($this->socket); + $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR); + $errstr = \socket_strerror($errno); + } else { + $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111; + $errstr = 'Connection refused'; + } + // @codeCoverageIgnoreEnd + + $this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno); + return; + } + + $this->readPending = true; + $this->loop->addReadStream($this->socket, array($this, 'handleRead')); + } + + $errno = 0; + $errstr = ''; + \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { + // Match errstr from PHP's warning message. + // fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe + \preg_match('/errno=(\d+) (.+)/', $error, $m); + $errno = isset($m[1]) ? (int) $m[1] : 0; + $errstr = isset($m[2]) ? $m[2] : $error; + }); + + $written = \fwrite($this->socket, $this->writeBuffer); + + \restore_error_handler(); + + if ($written === false || $written === 0) { + $this->closeError( + 'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')', + $errno + ); + return; + } + + if (isset($this->writeBuffer[$written])) { + $this->writeBuffer = \substr($this->writeBuffer, $written); + } else { + $this->loop->removeWriteStream($this->socket); + $this->writePending = false; + $this->writeBuffer = ''; + } + } + + /** + * @internal + */ + public function handleRead() + { + // read one chunk of data from the DNS server + // any error is fatal, this is a stream of TCP/IP data + $chunk = @\fread($this->socket, $this->readChunk); + if ($chunk === false || $chunk === '') { + $this->closeError('Connection to DNS server ' . $this->nameserver . ' lost'); + return; + } + + // reassemble complete message by concatenating all chunks. + $this->readBuffer .= $chunk; + + // response message header contains at least 12 bytes + while (isset($this->readBuffer[11])) { + // read response message length from first 2 bytes and ensure we have length + data in buffer + list(, $length) = \unpack('n', $this->readBuffer); + if (!isset($this->readBuffer[$length + 1])) { + return; + } + + $data = \substr($this->readBuffer, 2, $length); + $this->readBuffer = (string)substr($this->readBuffer, $length + 2); + + try { + $response = $this->parser->parseMessage($data); + } catch (\Exception $e) { + // reject all pending queries if we received an invalid message from remote server + $this->closeError('Invalid message received from DNS server ' . $this->nameserver); + return; + } + + // reject all pending queries if we received an unexpected response ID or truncated response + if (!isset($this->pending[$response->id]) || $response->tc) { + $this->closeError('Invalid response message received from DNS server ' . $this->nameserver); + return; + } + + $deferred = $this->pending[$response->id]; + unset($this->pending[$response->id], $this->names[$response->id]); + + $deferred->resolve($response); + + $this->checkIdle(); + } + } + + /** + * @internal + * @param string $reason + * @param int $code + */ + public function closeError($reason, $code = 0) + { + $this->readBuffer = ''; + if ($this->readPending) { + $this->loop->removeReadStream($this->socket); + $this->readPending = false; + } + + $this->writeBuffer = ''; + if ($this->writePending) { + $this->loop->removeWriteStream($this->socket); + $this->writePending = false; + } + + if ($this->idleTimer !== null) { + $this->loop->cancelTimer($this->idleTimer); + $this->idleTimer = null; + } + + @\fclose($this->socket); + $this->socket = null; + + foreach ($this->names as $id => $name) { + $this->pending[$id]->reject(new \RuntimeException( + 'DNS query for ' . $name . ' failed: ' . $reason, + $code + )); + } + $this->pending = $this->names = array(); + } + + /** + * @internal + */ + public function checkIdle() + { + if ($this->idleTimer === null && !$this->names) { + $that = $this; + $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) { + $that->closeError('Idle timeout'); + }); + } + } +} diff --git a/vendor/react/dns/src/Query/TimeoutException.php b/vendor/react/dns/src/Query/TimeoutException.php new file mode 100644 index 0000000..109b0a9 --- /dev/null +++ b/vendor/react/dns/src/Query/TimeoutException.php @@ -0,0 +1,7 @@ +executor = $executor; + $this->loop = $loop ?: Loop::get(); + $this->timeout = $timeout; + } + + public function query(Query $query) + { + $promise = $this->executor->query($query); + + $loop = $this->loop; + $time = $this->timeout; + return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $query) { + $timer = null; + $promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) { + if ($timer) { + $loop->cancelTimer($timer); + } + $timer = false; + $resolve($v); + }, function ($v) use (&$timer, $loop, $reject) { + if ($timer) { + $loop->cancelTimer($timer); + } + $timer = false; + $reject($v); + }); + + // promise already resolved => no need to start timer + if ($timer === false) { + return; + } + + // start timeout timer which will cancel the pending promise + $timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $query) { + $reject(new TimeoutException( + 'DNS query for ' . $query->describe() . ' timed out' + )); + + // Cancel pending query to clean up any underlying resources and references. + // Avoid garbage references in call stack by passing pending promise by reference. + assert(\method_exists($promise, 'cancel')); + $promise->cancel(); + $promise = null; + }); + }, function () use (&$promise) { + // Cancelling this promise will cancel the pending query, thus triggering the rejection logic above. + // Avoid garbage references in call stack by passing pending promise by reference. + assert(\method_exists($promise, 'cancel')); + $promise->cancel(); + $promise = null; + }); + } +} diff --git a/vendor/react/dns/src/Query/UdpTransportExecutor.php b/vendor/react/dns/src/Query/UdpTransportExecutor.php new file mode 100644 index 0000000..a8cbfaf --- /dev/null +++ b/vendor/react/dns/src/Query/UdpTransportExecutor.php @@ -0,0 +1,221 @@ +query( + * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) + * )->then(function (Message $message) { + * foreach ($message->answers as $answer) { + * echo 'IPv6: ' . $answer->data . PHP_EOL; + * } + * }, 'printf'); + * ``` + * + * See also the [fourth example](examples). + * + * Note that this executor does not implement a timeout, so you will very likely + * want to use this in combination with a `TimeoutExecutor` like this: + * + * ```php + * $executor = new TimeoutExecutor( + * new UdpTransportExecutor($nameserver), + * 3.0 + * ); + * ``` + * + * Also note that this executor uses an unreliable UDP transport and that it + * does not implement any retry logic, so you will likely want to use this in + * combination with a `RetryExecutor` like this: + * + * ```php + * $executor = new RetryExecutor( + * new TimeoutExecutor( + * new UdpTransportExecutor($nameserver), + * 3.0 + * ) + * ); + * ``` + * + * Note that this executor is entirely async and as such allows you to execute + * any number of queries concurrently. You should probably limit the number of + * concurrent queries in your application or you're very likely going to face + * rate limitations and bans on the resolver end. For many common applications, + * you may want to avoid sending the same query multiple times when the first + * one is still pending, so you will likely want to use this in combination with + * a `CoopExecutor` like this: + * + * ```php + * $executor = new CoopExecutor( + * new RetryExecutor( + * new TimeoutExecutor( + * new UdpTransportExecutor($nameserver), + * 3.0 + * ) + * ) + * ); + * ``` + * + * > Internally, this class uses PHP's UDP sockets and does not take advantage + * of [react/datagram](https://github.com/reactphp/datagram) purely for + * organizational reasons to avoid a cyclic dependency between the two + * packages. Higher-level components should take advantage of the Datagram + * component instead of reimplementing this socket logic from scratch. + */ +final class UdpTransportExecutor implements ExecutorInterface +{ + private $nameserver; + private $loop; + private $parser; + private $dumper; + + /** + * maximum UDP packet size to send and receive + * + * @var int + */ + private $maxPacketSize = 512; + + /** + * @param string $nameserver + * @param ?LoopInterface $loop + */ + public function __construct($nameserver, $loop = null) + { + if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) { + // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets + $nameserver = '[' . $nameserver . ']'; + } + + $parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver); + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || @\inet_pton(\trim($parts['host'], '[]')) === false) { + throw new \InvalidArgumentException('Invalid nameserver address given'); + } + + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); + } + + $this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); + $this->loop = $loop ?: Loop::get(); + $this->parser = new Parser(); + $this->dumper = new BinaryDumper(); + } + + public function query(Query $query) + { + $request = Message::createRequestForQuery($query); + + $queryData = $this->dumper->toBinary($request); + if (isset($queryData[$this->maxPacketSize])) { + return \React\Promise\reject(new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: Query too large for UDP transport', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 + )); + } + + // UDP connections are instant, so try connection without a loop or timeout + $errno = 0; + $errstr = ''; + $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0); + if ($socket === false) { + return \React\Promise\reject(new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', + $errno + )); + } + + // set socket to non-blocking and immediately try to send (fill write buffer) + \stream_set_blocking($socket, false); + + \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { + // Write may potentially fail, but most common errors are already caught by connection check above. + // Among others, macOS is known to report here when trying to send to broadcast address. + // This can also be reproduced by writing data exceeding `stream_set_chunk_size()` to a server refusing UDP data. + // fwrite(): send of 8192 bytes failed with errno=111 Connection refused + \preg_match('/errno=(\d+) (.+)/', $error, $m); + $errno = isset($m[1]) ? (int) $m[1] : 0; + $errstr = isset($m[2]) ? $m[2] : $error; + }); + + $written = \fwrite($socket, $queryData); + + \restore_error_handler(); + + if ($written !== \strlen($queryData)) { + return \React\Promise\reject(new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')', + $errno + )); + } + + $loop = $this->loop; + $deferred = new Deferred(function () use ($loop, $socket, $query) { + // cancellation should remove socket from loop and close socket + $loop->removeReadStream($socket); + \fclose($socket); + + throw new CancellationException('DNS query for ' . $query->describe() . ' has been cancelled'); + }); + + $max = $this->maxPacketSize; + $parser = $this->parser; + $nameserver = $this->nameserver; + $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max, $nameserver) { + // try to read a single data packet from the DNS server + // ignoring any errors, this is uses UDP packets and not a stream of data + $data = @\fread($socket, $max); + if ($data === false) { + return; + } + + try { + $response = $parser->parseMessage($data); + } catch (\Exception $e) { + // ignore and await next if we received an invalid message from remote server + // this may as well be a fake response from an attacker (possible DOS) + return; + } + + // ignore and await next if we received an unexpected response ID + // this may as well be a fake response from an attacker (possible cache poisoning) + if ($response->id !== $request->id) { + return; + } + + // we only react to the first valid message, so remove socket from loop and close + $loop->removeReadStream($socket); + \fclose($socket); + + if ($response->tc) { + $deferred->reject(new \RuntimeException( + 'DNS query for ' . $query->describe() . ' failed: The DNS server ' . $nameserver . ' returned a truncated result for a UDP query', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 + )); + return; + } + + $deferred->resolve($response); + }); + + return $deferred->promise(); + } +} diff --git a/vendor/react/dns/src/RecordNotFoundException.php b/vendor/react/dns/src/RecordNotFoundException.php new file mode 100644 index 0000000..3b70274 --- /dev/null +++ b/vendor/react/dns/src/RecordNotFoundException.php @@ -0,0 +1,7 @@ +decorateHostsFileExecutor($this->createExecutor($config, $loop ?: Loop::get())); + + return new Resolver($executor); + } + + /** + * Creates a cached DNS resolver instance for the given DNS config and cache + * + * As of v1.7.0 it's recommended to pass a `Config` object instead of a + * single nameserver address. If the given config contains more than one DNS + * nameserver, all DNS nameservers will be used in order. The primary DNS + * server will always be used first before falling back to the secondary or + * tertiary DNS server. + * + * @param Config|string $config DNS Config object (recommended) or single nameserver address + * @param ?LoopInterface $loop + * @param ?CacheInterface $cache + * @return \React\Dns\Resolver\ResolverInterface + * @throws \InvalidArgumentException for invalid DNS server address + * @throws \UnderflowException when given DNS Config object has an empty list of nameservers + */ + public function createCached($config, $loop = null, $cache = null) + { + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); + } + + if ($cache !== null && !$cache instanceof CacheInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #3 ($cache) expected null|React\Cache\CacheInterface'); + } + + // default to keeping maximum of 256 responses in cache unless explicitly given + if (!($cache instanceof CacheInterface)) { + $cache = new ArrayCache(256); + } + + $executor = $this->createExecutor($config, $loop ?: Loop::get()); + $executor = new CachingExecutor($executor, $cache); + $executor = $this->decorateHostsFileExecutor($executor); + + return new Resolver($executor); + } + + /** + * Tries to load the hosts file and decorates the given executor on success + * + * @param ExecutorInterface $executor + * @return ExecutorInterface + * @codeCoverageIgnore + */ + private function decorateHostsFileExecutor(ExecutorInterface $executor) + { + try { + $executor = new HostsFileExecutor( + HostsFile::loadFromPathBlocking(), + $executor + ); + } catch (\RuntimeException $e) { + // ignore this file if it can not be loaded + } + + // Windows does not store localhost in hosts file by default but handles this internally + // To compensate for this, we explicitly use hard-coded defaults for localhost + if (DIRECTORY_SEPARATOR === '\\') { + $executor = new HostsFileExecutor( + new HostsFile("127.0.0.1 localhost\n::1 localhost"), + $executor + ); + } + + return $executor; + } + + /** + * @param Config|string $nameserver + * @param LoopInterface $loop + * @return CoopExecutor + * @throws \InvalidArgumentException for invalid DNS server address + * @throws \UnderflowException when given DNS Config object has an empty list of nameservers + */ + private function createExecutor($nameserver, LoopInterface $loop) + { + if ($nameserver instanceof Config) { + if (!$nameserver->nameservers) { + throw new \UnderflowException('Empty config with no DNS servers'); + } + + // Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config). + // Note to future self: Recursion isn't too hard, but how deep do we really want to go? + $primary = reset($nameserver->nameservers); + $secondary = next($nameserver->nameservers); + $tertiary = next($nameserver->nameservers); + + if ($tertiary !== false) { + // 3 DNS servers given => nest first with fallback for second and third + return new CoopExecutor( + new RetryExecutor( + new FallbackExecutor( + $this->createSingleExecutor($primary, $loop), + new FallbackExecutor( + $this->createSingleExecutor($secondary, $loop), + $this->createSingleExecutor($tertiary, $loop) + ) + ) + ) + ); + } elseif ($secondary !== false) { + // 2 DNS servers given => fallback from first to second + return new CoopExecutor( + new RetryExecutor( + new FallbackExecutor( + $this->createSingleExecutor($primary, $loop), + $this->createSingleExecutor($secondary, $loop) + ) + ) + ); + } else { + // 1 DNS server given => use single executor + $nameserver = $primary; + } + } + + return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop))); + } + + /** + * @param string $nameserver + * @param LoopInterface $loop + * @return ExecutorInterface + * @throws \InvalidArgumentException for invalid DNS server address + */ + private function createSingleExecutor($nameserver, LoopInterface $loop) + { + $parts = \parse_url($nameserver); + + if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') { + $executor = $this->createTcpExecutor($nameserver, $loop); + } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') { + $executor = $this->createUdpExecutor($nameserver, $loop); + } else { + $executor = new SelectiveTransportExecutor( + $this->createUdpExecutor($nameserver, $loop), + $this->createTcpExecutor($nameserver, $loop) + ); + } + + return $executor; + } + + /** + * @param string $nameserver + * @param LoopInterface $loop + * @return TimeoutExecutor + * @throws \InvalidArgumentException for invalid DNS server address + */ + private function createTcpExecutor($nameserver, LoopInterface $loop) + { + return new TimeoutExecutor( + new TcpTransportExecutor($nameserver, $loop), + 5.0, + $loop + ); + } + + /** + * @param string $nameserver + * @param LoopInterface $loop + * @return TimeoutExecutor + * @throws \InvalidArgumentException for invalid DNS server address + */ + private function createUdpExecutor($nameserver, LoopInterface $loop) + { + return new TimeoutExecutor( + new UdpTransportExecutor( + $nameserver, + $loop + ), + 5.0, + $loop + ); + } +} diff --git a/vendor/react/dns/src/Resolver/Resolver.php b/vendor/react/dns/src/Resolver/Resolver.php new file mode 100644 index 0000000..92926f3 --- /dev/null +++ b/vendor/react/dns/src/Resolver/Resolver.php @@ -0,0 +1,147 @@ +executor = $executor; + } + + public function resolve($domain) + { + return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) { + return $ips[array_rand($ips)]; + }); + } + + public function resolveAll($domain, $type) + { + $query = new Query($domain, $type, Message::CLASS_IN); + $that = $this; + + return $this->executor->query( + $query + )->then(function (Message $response) use ($query, $that) { + return $that->extractValues($query, $response); + }); + } + + /** + * [Internal] extract all resource record values from response for this query + * + * @param Query $query + * @param Message $response + * @return array + * @throws RecordNotFoundException when response indicates an error or contains no data + * @internal + */ + public function extractValues(Query $query, Message $response) + { + // reject if response code indicates this is an error response message + $code = $response->rcode; + if ($code !== Message::RCODE_OK) { + switch ($code) { + case Message::RCODE_FORMAT_ERROR: + $message = 'Format Error'; + break; + case Message::RCODE_SERVER_FAILURE: + $message = 'Server Failure'; + break; + case Message::RCODE_NAME_ERROR: + $message = 'Non-Existent Domain / NXDOMAIN'; + break; + case Message::RCODE_NOT_IMPLEMENTED: + $message = 'Not Implemented'; + break; + case Message::RCODE_REFUSED: + $message = 'Refused'; + break; + default: + $message = 'Unknown error response code ' . $code; + } + throw new RecordNotFoundException( + 'DNS query for ' . $query->describe() . ' returned an error response (' . $message . ')', + $code + ); + } + + $answers = $response->answers; + $addresses = $this->valuesByNameAndType($answers, $query->name, $query->type); + + // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found) + if (0 === count($addresses)) { + throw new RecordNotFoundException( + 'DNS query for ' . $query->describe() . ' did not return a valid answer (NOERROR / NODATA)' + ); + } + + return array_values($addresses); + } + + /** + * @param \React\Dns\Model\Record[] $answers + * @param string $name + * @param int $type + * @return array + */ + private function valuesByNameAndType(array $answers, $name, $type) + { + // return all record values for this name and type (if any) + $named = $this->filterByName($answers, $name); + $records = $this->filterByType($named, $type); + if ($records) { + return $this->mapRecordData($records); + } + + // no matching records found? check if there are any matching CNAMEs instead + $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME); + if ($cnameRecords) { + $cnames = $this->mapRecordData($cnameRecords); + foreach ($cnames as $cname) { + $records = array_merge( + $records, + $this->valuesByNameAndType($answers, $cname, $type) + ); + } + } + + return $records; + } + + private function filterByName(array $answers, $name) + { + return $this->filterByField($answers, 'name', $name); + } + + private function filterByType(array $answers, $type) + { + return $this->filterByField($answers, 'type', $type); + } + + private function filterByField(array $answers, $field, $value) + { + $value = strtolower($value); + return array_filter($answers, function ($answer) use ($field, $value) { + return $value === strtolower($answer->$field); + }); + } + + private function mapRecordData(array $records) + { + return array_map(function ($record) { + return $record->data; + }, $records); + } +} diff --git a/vendor/react/dns/src/Resolver/ResolverInterface.php b/vendor/react/dns/src/Resolver/ResolverInterface.php new file mode 100644 index 0000000..555a1cb --- /dev/null +++ b/vendor/react/dns/src/Resolver/ResolverInterface.php @@ -0,0 +1,94 @@ +resolve('reactphp.org')->then(function ($ip) { + * echo 'IP for reactphp.org is ' . $ip . PHP_EOL; + * }); + * ``` + * + * This is one of the main methods in this package. It sends a DNS query + * for the given $domain name to your DNS server and returns a single IP + * address on success. + * + * If the DNS server sends a DNS response message that contains more than + * one IP address for this query, it will randomly pick one of the IP + * addresses from the response. If you want the full list of IP addresses + * or want to send a different type of query, you should use the + * [`resolveAll()`](#resolveall) method instead. + * + * If the DNS server sends a DNS response message that indicates an error + * code, this method will reject with a `RecordNotFoundException`. Its + * message and code can be used to check for the response code. + * + * If the DNS communication fails and the server does not respond with a + * valid response message, this message will reject with an `Exception`. + * + * Pending DNS queries can be cancelled by cancelling its pending promise like so: + * + * ```php + * $promise = $resolver->resolve('reactphp.org'); + * + * $promise->cancel(); + * ``` + * + * @param string $domain + * @return \React\Promise\PromiseInterface + * resolves with a single IP address on success or rejects with an Exception on error. + */ + public function resolve($domain); + + /** + * Resolves all record values for the given $domain name and query $type. + * + * ```php + * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { + * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; + * }); + * + * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { + * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; + * }); + * ``` + * + * This is one of the main methods in this package. It sends a DNS query + * for the given $domain name to your DNS server and returns a list with all + * record values on success. + * + * If the DNS server sends a DNS response message that contains one or more + * records for this query, it will return a list with all record values + * from the response. You can use the `Message::TYPE_*` constants to control + * which type of query will be sent. Note that this method always returns a + * list of record values, but each record value type depends on the query + * type. For example, it returns the IPv4 addresses for type `A` queries, + * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, + * `CNAME` and `PTR` queries and structured data for other queries. See also + * the `Record` documentation for more details. + * + * If the DNS server sends a DNS response message that indicates an error + * code, this method will reject with a `RecordNotFoundException`. Its + * message and code can be used to check for the response code. + * + * If the DNS communication fails and the server does not respond with a + * valid response message, this message will reject with an `Exception`. + * + * Pending DNS queries can be cancelled by cancelling its pending promise like so: + * + * ```php + * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); + * + * $promise->cancel(); + * ``` + * + * @param string $domain + * @return \React\Promise\PromiseInterface + * Resolves with all record values on success or rejects with an Exception on error. + */ + public function resolveAll($domain, $type); +} diff --git a/vendor/react/http/CHANGELOG.md b/vendor/react/http/CHANGELOG.md new file mode 100644 index 0000000..07a4cc6 --- /dev/null +++ b/vendor/react/http/CHANGELOG.md @@ -0,0 +1,920 @@ +# Changelog + +## 1.11.0 (2024-11-20) + +* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable types. + (#537 by @clue) + +* Feature: Allow underscore character in Uri host. + (#524 by @lulhum) + +* Improve test suite to fix expected error code when ext-sockets is not enabled. + (#539 by @WyriHaximus) + +## 1.10.0 (2024-03-27) + +* Feature: Add new PSR-7 implementation and remove dated RingCentral PSR-7 dependency. + (#518, #519, #520 and #522 by @clue) + + This changeset allows us to maintain our own PSR-7 implementation and reduce + dependencies on external projects. It also improves performance slightly and + does not otherwise affect our public API. If you want to explicitly install + the old RingCentral PSR-7 dependency, you can still install it like this: + + ```bash + composer require ringcentral/psr7 + ``` + +* Feature: Add new `Uri` class for new PSR-7 implementation. + (#521 by @clue) + +* Feature: Validate outgoing HTTP message headers and reject invalid messages. + (#523 by @clue) + +* Feature: Full PHP 8.3 compatibility. + (#508 by @clue) + +* Fix: Fix HTTP client to omit `Transfer-Encoding: chunked` when streaming empty request body. + (#516 by @clue) + +* Fix: Ensure connection close handler is cleaned up for each request. + (#515 by @WyriHaximus) + +* Update test suite and avoid unhandled promise rejections. + (#501 and #502 by @clue) + +## 1.9.0 (2023-04-26) + +This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component. + +* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component + that affects all versions between `v0.8.0` and `v1.8.0`. All users are encouraged to upgrade immediately. + (CVE-2023-26044 reported and fixed by @WyriHaximus) + +* Feature: Support HTTP keep-alive for HTTP client (reusing persistent connections). + (#481, #484, #486 and #495 by @clue) + + This feature offers significant performance improvements when sending many + requests to the same host as it avoids recreating the underlying TCP/IP + connection and repeating the TLS handshake for secure HTTPS requests. + + ```php + $browser = new React\Http\Browser(); + + // Up to 300% faster! HTTP keep-alive is enabled by default + $response = React\Async\await($browser->get('https://httpbingo.org/redirect/6')); + assert($response instanceof Psr\Http\Message\ResponseInterface); + ``` + +* Feature: Add `Request` class to represent outgoing HTTP request message. + (#480 by @clue) + +* Feature: Preserve request method and body for `307 Temporary Redirect` and `308 Permanent Redirect`. + (#442 by @dinooo13) + +* Feature: Include buffer logic to avoid dependency on reactphp/promise-stream. + (#482 by @clue) + +* Improve test suite and project setup and report failed assertions. + (#478 by @clue, #487 and #491 by @WyriHaximus and #475 and #479 by @SimonFrings) + +## 1.8.0 (2022-09-29) + +* Feature: Support for default request headers. + (#461 by @51imyy) + + ```php + $browser = new React\Http\Browser(); + $browser = $browser->withHeader('User-Agent', 'ACME'); + + $browser->get($url)->then(…); + ``` + +* Feature: Forward compatibility with upcoming Promise v3. + (#460 by @clue) + +## 1.7.0 (2022-08-23) + +This is a **SECURITY** and feature release for the 1.x series of ReactPHP's HTTP component. + +* Security fix: This release fixes a medium severity security issue in ReactPHP's HTTP server component + that affects all versions between `v0.7.0` and `v1.6.0`. All users are encouraged to upgrade immediately. + Special thanks to Marco Squarcina (TU Wien) for reporting this and working with us to coordinate this release. + (CVE-2022-36032 reported by @lavish and fixed by @clue) + +* Feature: Improve HTTP server performance by ~20%, reuse syscall values for clock time and socket addresses. + (#457 and #467 by @clue) + +* Feature: Full PHP 8.2+ compatibility, refactor internal `Transaction` to avoid assigning dynamic properties. + (#459 by @clue and #466 by @WyriHaximus) + +* Feature / Fix: Allow explicit `Content-Length` response header on `HEAD` requests. + (#444 by @mrsimonbennett) + +* Minor documentation improvements. + (#452 by @clue, #458 by @nhedger, #448 by @jorrit and #446 by @SimonFrings) + +* Improve test suite, update to use new reactphp/async package instead of clue/reactphp-block, + skip memory tests when lowering memory limit fails and fix legacy HHVM build. + (#464 and #440 by @clue and #450 by @SimonFrings) + +## 1.6.0 (2022-02-03) + +* Feature: Add factory methods for common HTML/JSON/plaintext/XML response types. + (#439 by @clue) + + ```php + $response = React\Http\Response\html("

Hello wörld!

\n"); + $response = React\Http\Response\json(['message' => 'Hello wörld!']); + $response = React\Http\Response\plaintext("Hello wörld!\n"); + $response = React\Http\Response\xml("Hello wörld!\n"); + ``` + +* Feature: Expose all status code constants via `Response` class. + (#432 by @clue) + + ```php + $response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, // 200 OK + … + ); + $response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_NOT_FOUND, // 404 Not Found + … + ); + ``` + +* Feature: Full support for PHP 8.1 release. + (#433 by @SimonFrings and #434 by @clue) + +* Feature / Fix: Improve protocol handling for HTTP responses with no body. + (#429 and #430 by @clue) + +* Internal refactoring and internal improvements for handling requests and responses. + (#422 by @WyriHaximus and #431 by @clue) + +* Improve documentation, update proxy examples, include error reporting in examples. + (#420, #424, #426, and #427 by @clue) + +* Update test suite to use default loop. + (#438 by @clue) + +## 1.5.0 (2021-08-04) + +* Feature: Update `Browser` signature to take optional `$connector` as first argument and + to match new Socket API without nullable loop arguments. + (#418 and #419 by @clue) + + ```php + // unchanged + $browser = new React\Http\Browser(); + + // deprecated + $browser = new React\Http\Browser(null, $connector); + $browser = new React\Http\Browser($loop, $connector); + + // new + $browser = new React\Http\Browser($connector); + $browser = new React\Http\Browser($connector, $loop); + ``` + +* Feature: Rename `Server` to `HttpServer` to avoid class name collisions and + to avoid any ambiguities with regards to the new `SocketServer` API. + (#417 and #419 by @clue) + + ```php + // deprecated + $server = new React\Http\Server($handler); + $server->listen(new React\Socket\Server(8080)); + + // new + $http = new React\Http\HttpServer($handler); + $http->listen(new React\Socket\SocketServer('127.0.0.1:8080')); + ``` + +## 1.4.0 (2021-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). + (#410 by @clue) + + ```php + // old (still supported) + $browser = new React\Http\Browser($loop); + $server = new React\Http\Server($loop, $handler); + + // new (using default loop) + $browser = new React\Http\Browser(); + $server = new React\Http\Server($handler); + ``` + +## 1.3.0 (2021-04-11) + +* Feature: Support persistent connections (`Connection: keep-alive`). + (#405 by @clue) + + This shows a noticeable performance improvement especially when benchmarking + using persistent connections (which is the default pretty much everywhere). + Together with other changes in this release, this improves benchmarking + performance by around 100%. + +* Feature: Require `Host` request header for HTTP/1.1 requests. + (#404 by @clue) + +* Minor documentation improvements. + (#398 by @fritz-gerneth and #399 and #400 by @pavog) + +* Improve test suite, use GitHub actions for continuous integration (CI). + (#402 by @SimonFrings) + +## 1.2.0 (2020-12-04) + +* Feature: Keep request body in memory also after consuming request body. + (#395 by @clue) + + This means consumers can now always access the complete request body as + detailed in the documentation. This allows building custom parsers and more + advanced processing models without having to mess with the default parsers. + +## 1.1.0 (2020-09-11) + +* Feature: Support upcoming PHP 8 release, update to reactphp/socket v1.6 and adjust type checks for invalid chunk headers. + (#391 by @clue) + +* Feature: Consistently resolve base URL according to HTTP specs. + (#379 by @clue) + +* Feature / Fix: Expose `Transfer-Encoding: chunked` response header and fix chunked responses for `HEAD` requests. + (#381 by @clue) + +* Internal refactoring to remove unneeded `MessageFactory` and `Response` classes. + (#380 and #389 by @clue) + +* Minor documentation improvements and improve test suite, update to support PHPUnit 9.3. + (#385 by @clue and #393 by @SimonFrings) + +## 1.0.0 (2020-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2020/announcing-reactphp-http). + +* First stable LTS release, now following [SemVer](https://semver.org/). + We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +This update involves some major new features and a number of BC breaks due to +some necessary API cleanup. We've tried hard to avoid BC breaks where possible +and minimize impact otherwise. We expect that most consumers of this package +will be affected by BC breaks, but updating should take no longer than a few +minutes. See below for more details: + +* Feature: Add async HTTP client implementation. + (#368 by @clue) + + ```php + $browser = new React\Http\Browser($loop); + $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + echo $response->getBody(); + }); + ``` + + The code has been imported as-is from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz), + with only minor changes to the namespace and we otherwise leave all the existing APIs unchanged. + Upgrading from [clue/reactphp-buzz v2.9.0](https://github.com/clue/reactphp-buzz) + to this release should be a matter of updating some namespace references only: + + ```php + // old + $browser = new Clue\React\Buzz\Browser($loop); + + // new + $browser = new React\Http\Browser($loop); + ``` + +* Feature / BC break: Add `LoopInterface` as required first constructor argument to `Server` and + change `Server` to accept variadic middleware handlers instead of `array`. + (#361 and #362 by @WyriHaximus) + + ```php + // old + $server = new React\Http\Server($handler); + $server = new React\Http\Server([$middleware, $handler]); + + // new + $server = new React\Http\Server($loop, $handler); + $server = new React\Http\Server($loop, $middleware, $handler); + ``` + +* Feature / BC break: Move `Response` class to `React\Http\Message\Response` and + expose `ServerRequest` class to `React\Http\Message\ServerRequest`. + (#370 by @clue) + + ```php + // old + $response = new React\Http\Response(200, [], 'Hello!'); + + // new + $response = new React\Http\Message\Response(200, [], 'Hello!'); + ``` + +* Feature / BC break: Add `StreamingRequestMiddleware` to stream incoming requests, mark `StreamingServer` as internal. + (#367 by @clue) + + ```php + // old: advanced StreamingServer is now internal only + $server = new React\Http\StreamingServer($handler); + + // new: use StreamingRequestMiddleware instead of StreamingServer + $server = new React\Http\Server( + $loop, + new React\Http\Middleware\StreamingRequestMiddleware(), + $handler + ); + ``` + +* Feature / BC break: Improve default concurrency to 1024 requests and cap default request buffer at 64K. + (#371 by @clue) + + This improves default concurrency to 1024 requests and caps the default request buffer at 64K. + The previous defaults resulted in just 4 concurrent requests with a request buffer of 8M. + See [`Server`](README.md#server) for details on how to override these defaults. + +* Feature: Expose ReactPHP in `User-Agent` client-side request header and in `Server` server-side response header. + (#374 by @clue) + +* Mark all classes as `final` to discourage inheriting from it. + (#373 by @WyriHaximus) + +* Improve documentation and use fully-qualified class names throughout the documentation and + add ReactPHP core team as authors to `composer.json` and license file. + (#366 and #369 by @WyriHaximus and #375 by @clue) + +* Improve test suite and support skipping all online tests with `--exclude-group internet`. + (#372 by @clue) + +## 0.8.7 (2020-07-05) + +* Fix: Fix parsing multipart request body with quoted header parameters (dot net). + (#363 by @ebimmel) + +* Fix: Fix calculating concurrency when `post_max_size` ini is unlimited. + (#365 by @clue) + +* Improve test suite to run tests on PHPUnit 9 and clean up test suite. + (#364 by @SimonFrings) + +## 0.8.6 (2020-01-12) + +* Fix: Fix parsing `Cookie` request header with comma in its values. + (#352 by @fiskie) + +* Fix: Avoid unneeded warning when decoding invalid data on PHP 7.4. + (#357 by @WyriHaximus) + +* Add .gitattributes to exclude dev files from exports. + (#353 by @reedy) + +## 0.8.5 (2019-10-29) + +* Internal refactorings and optimizations to improve request parsing performance. + Benchmarks suggest number of requests/s improved by ~30% for common `GET` requests. + (#345, #346, #349 and #350 by @clue) + +* Add documentation and example for JSON/XML request body and + improve documentation for concurrency and streaming requests and for error handling. + (#341 and #342 by @clue) + +## 0.8.4 (2019-01-16) + +* Improvement: Internal refactoring to simplify response header logic. + (#321 by @clue) + +* Improvement: Assign Content-Length response header automatically only when size is known. + (#329 by @clue) + +* Improvement: Import global functions for better performance. + (#330 by @WyriHaximus) + +## 0.8.3 (2018-04-11) + +* Feature: Do not pause connection stream to detect closed connections immediately. + (#315 by @clue) + +* Feature: Keep incoming `Transfer-Encoding: chunked` request header. + (#316 by @clue) + +* Feature: Reject invalid requests that contain both `Content-Length` and `Transfer-Encoding` request headers. + (#318 by @clue) + +* Minor internal refactoring to simplify connection close logic after sending response. + (#317 by @clue) + +## 0.8.2 (2018-04-06) + +* Fix: Do not pass `$next` handler to final request handler. + (#308 by @clue) + +* Fix: Fix awaiting queued handlers when cancelling a queued handler. + (#313 by @clue) + +* Fix: Fix Server to skip `SERVER_ADDR` params for Unix domain sockets (UDS). + (#307 by @clue) + +* Documentation for PSR-15 middleware and minor documentation improvements. + (#314 by @clue and #297, #298 and #310 by @seregazhuk) + +* Minor code improvements and micro optimizations. + (#301 by @seregazhuk and #305 by @kalessil) + +## 0.8.1 (2018-01-05) + +* Major request handler performance improvement. Benchmarks suggest number of + requests/s improved by more than 50% for common `GET` requests! + We now avoid queuing, buffering and wrapping incoming requests in promises + when we're below limits and instead can directly process common requests. + (#291, #292, #293, #294 and #296 by @clue) + +* Fix: Fix concurrent invoking next middleware request handlers + (#293 by @clue) + +* Small code improvements + (#286 by @seregazhuk) + +* Improve test suite to be less fragile when using `ext-event` and + fix test suite forward compatibility with upcoming EventLoop releases + (#288 and #290 by @clue) + +## 0.8.0 (2017-12-12) + +* Feature / BC break: Add new `Server` facade that buffers and parses incoming + HTTP requests. This provides full PSR-7 compatibility, including support for + form submissions with POST fields and file uploads. + The old `Server` has been renamed to `StreamingServer` for advanced usage + and is used internally. + (#266, #271, #281, #282, #283 and #284 by @WyriHaximus and @clue) + + ```php + // old: handle incomplete/streaming requests + $server = new Server($handler); + + // new: handle complete, buffered and parsed requests + // new: full PSR-7 support, including POST fields and file uploads + $server = new Server($handler); + + // new: handle incomplete/streaming requests + $server = new StreamingServer($handler); + ``` + + > While this is technically a small BC break, this should in fact not break + most consuming code. If you rely on the old request streaming, you can + explicitly use the advanced `StreamingServer` to restore old behavior. + +* Feature: Add support for middleware request handler arrays + (#215, #228, #229, #236, #237, #238, #246, #247, #277, #279 and #285 by @WyriHaximus, @clue and @jsor) + + ```php + // new: middleware request handler arrays + $server = new Server(array( + function (ServerRequestInterface $request, callable $next) { + $request = $request->withHeader('Processed', time()); + return $next($request); + }, + function (ServerRequestInterface $request) { + return new Response(); + } + )); + ``` + +* Feature: Add support for limiting how many next request handlers can be + executed concurrently (`LimitConcurrentRequestsMiddleware`) + (#272 by @clue and @WyriHaximus) + + ```php + // new: explicitly limit concurrency + $server = new Server(array( + new LimitConcurrentRequestsMiddleware(10), + $handler + )); + ``` + +* Feature: Add support for buffering the incoming request body + (`RequestBodyBufferMiddleware`). + This feature mimics PHP's default behavior and respects its `post_max_size` + ini setting by default and allows explicit configuration. + (#216, #224, #263, #276 and #278 by @WyriHaximus and #235 by @andig) + + ```php + // new: buffer up to 10 requests with 8 MiB each + $server = new StreamingServer(array( + new LimitConcurrentRequestsMiddleware(10), + new RequestBodyBufferMiddleware('8M'), + $handler + )); + ``` + +* Feature: Add support for parsing form submissions with POST fields and file + uploads (`RequestBodyParserMiddleware`). + This feature mimics PHP's default behavior and respects its ini settings and + `MAX_FILE_SIZE` POST fields by default and allows explicit configuration. + (#220, #226, #252, #261, #264, #265, #267, #268, #274 by @WyriHaximus and @clue) + + ```php + // new: buffer up to 10 requests with 8 MiB each + // and limit to 4 uploads with 2 MiB each + $server = new StreamingServer(array( + new LimitConcurrentRequestsMiddleware(10), + new RequestBodyBufferMiddleware('8M'), + new RequestBodyParserMiddleware('2M', 4) + $handler + )); + ``` + +* Feature: Update Socket to work around sending secure HTTPS responses with PHP < 7.1.4 + (#244 by @clue) + +* Feature: Support sending same response header multiple times (e.g. `Set-Cookie`) + (#248 by @clue) + +* Feature: Raise maximum request header size to 8k to match common implementations + (#253 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit 6, test + against PHP 7.1 and PHP 7.2 and refactor and remove risky and duplicate tests. + (#243, #269 and #270 by @carusogabriel and #249 by @clue) + +* Minor code refactoring to move internal classes to `React\Http\Io` namespace + and clean up minor code and documentation issues + (#251 by @clue, #227 by @kalessil, #240 by @christoph-kluge, #230 by @jsor and #280 by @andig) + +## 0.7.4 (2017-08-16) + +* Improvement: Target evenement 3.0 a long side 2.0 and 1.0 + (#212 by @WyriHaximus) + +## 0.7.3 (2017-08-14) + +* Feature: Support `Throwable` when setting previous exception from server callback + (#155 by @jsor) + +* Fix: Fixed URI parsing for origin-form requests that contain scheme separator + such as `/path?param=http://example.com`. + (#209 by @aaronbonneau) + +* Improve test suite by locking Travis distro so new defaults will not break the build + (#211 by @clue) + +## 0.7.2 (2017-07-04) + +* Fix: Stricter check for invalid request-line in HTTP requests + (#206 by @clue) + +* Refactor to use HTTP response reason phrases from response object + (#205 by @clue) + +## 0.7.1 (2017-06-17) + +* Fix: Fix parsing CONNECT request without `Host` header + (#201 by @clue) + +* Internal preparation for future PSR-7 `UploadedFileInterface` + (#199 by @WyriHaximus) + +## 0.7.0 (2017-05-29) + +* Feature / BC break: Use PSR-7 (http-message) standard and + `Request-In-Response-Out`-style request handler callback. + Pass standard PSR-7 `ServerRequestInterface` and expect any standard + PSR-7 `ResponseInterface` in return for the request handler callback. + (#146 and #152 and #170 by @legionth) + + ```php + // old + $app = function (Request $request, Response $response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("Hello world!\n"); + }; + + // new + $app = function (ServerRequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + "Hello world!\n" + ); + }; + ``` + + A `Content-Length` header will automatically be included if the size can be + determined from the response body. + (#164 by @maciejmrozinski) + + The request handler callback will automatically make sure that responses to + HEAD requests and certain status codes, such as `204` (No Content), never + contain a response body. + (#156 by @clue) + + The intermediary `100 Continue` response will automatically be sent if + demanded by a HTTP/1.1 client. + (#144 by @legionth) + + The request handler callback can now return a standard `Promise` if + processing the request needs some time, such as when querying a database. + Similarly, the request handler may return a streaming response if the + response body comes from a `ReadableStreamInterface` or its size is + unknown in advance. + + ```php + // old + $app = function (Request $request, Response $response) use ($db) { + $db->query()->then(function ($result) use ($response) { + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end($result); + }); + }; + + // new + $app = function (ServerRequestInterface $request) use ($db) { + return $db->query()->then(function ($result) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $result + ); + }); + }; + ``` + + Pending promies and response streams will automatically be canceled once the + client connection closes. + (#187 and #188 by @clue) + + The `ServerRequestInterface` contains the full effective request URI, + server-side parameters, query parameters and parsed cookies values as + defined in PSR-7. + (#167 by @clue and #174, #175 and #180 by @legionth) + + ```php + $app = function (ServerRequestInterface $request) { + return new Response( + 200, + array('Content-Type' => 'text/plain'), + $request->getUri()->getScheme() + ); + }; + ``` + + Advanced: Support duplex stream response for `Upgrade` requests such as + `Upgrade: WebSocket` or custom protocols and `CONNECT` requests + (#189 and #190 by @clue) + + > Note that the request body will currently not be buffered and parsed by + default, which depending on your particilar use-case, may limit + interoperability with the PSR-7 (http-message) ecosystem. + The provided streaming request body interfaces allow you to perform + buffering and parsing as needed in the request handler callback. + See also the README and examples for more details. + +* Feature / BC break: Replace `request` listener with callback function and + use `listen()` method to support multiple listening sockets + (#97 by @legionth and #193 by @clue) + + ```php + // old + $server = new Server($socket); + $server->on('request', $app); + + // new + $server = new Server($app); + $server->listen($socket); + ``` + +* Feature: Support the more advanced HTTP requests, such as + `OPTIONS * HTTP/1.1` (`OPTIONS` method in asterisk-form), + `GET http://example.com/path HTTP/1.1` (plain proxy requests in absolute-form), + `CONNECT example.com:443 HTTP/1.1` (`CONNECT` proxy requests in authority-form) + and sanitize `Host` header value across all requests. + (#157, #158, #161, #165, #169 and #173 by @clue) + +* Feature: Forward compatibility with Socket v1.0, v0.8, v0.7 and v0.6 and + forward compatibility with Stream v1.0 and v0.7 + (#154, #163, #183, #184 and #191 by @clue) + +* Feature: Simplify examples to ease getting started and + add benchmarking example + (#151 and #162 by @clue) + +* Improve test suite by adding tests for case insensitive chunked transfer + encoding and ignoring HHVM test failures until Travis tests work again. + (#150 by @legionth and #185 by @clue) + +## 0.6.0 (2017-03-09) + +* Feature / BC break: The `Request` and `Response` objects now follow strict + stream semantics and their respective methods and events. + (#116, #129, #133, #135, #136, #137, #138, #140, #141 by @legionth + and #122, #123, #130, #131, #132, #142 by @clue) + + This implies that the `Server` now supports proper detection of the request + message body stream, such as supporting decoding chunked transfer encoding, + delimiting requests with an explicit `Content-Length` header + and those with an empty request message body. + + These streaming semantics are compatible with previous Stream v0.5, future + compatible with v0.5 and upcoming v0.6 versions and can be used like this: + + ```php + $http->on('request', function (Request $request, Response $response) { + $contentLength = 0; + $request->on('data', function ($data) use (&$contentLength) { + $contentLength += strlen($data); + }); + + $request->on('end', function () use ($response, &$contentLength){ + $response->writeHead(200, array('Content-Type' => 'text/plain')); + $response->end("The length of the submitted request body is: " . $contentLength); + }); + + // an error occured + // e.g. on invalid chunked encoded data or an unexpected 'end' event + $request->on('error', function (\Exception $exception) use ($response, &$contentLength) { + $response->writeHead(400, array('Content-Type' => 'text/plain')); + $response->end("An error occured while reading at length: " . $contentLength); + }); + }); + ``` + + Similarly, the `Request` and `Response` now strictly follow the + `close()` method and `close` event semantics. + Closing the `Request` does not interrupt the underlying TCP/IP in + order to allow still sending back a valid response message. + Closing the `Response` does terminate the underlying TCP/IP + connection in order to clean up resources. + + You should make sure to always attach a `request` event listener + like above. The `Server` will not respond to an incoming HTTP + request otherwise and keep the TCP/IP connection pending until the + other side chooses to close the connection. + +* Feature: Support `HTTP/1.1` and `HTTP/1.0` for `Request` and `Response`. + (#124, #125, #126, #127, #128 by @clue and #139 by @legionth) + + The outgoing `Response` will automatically use the same HTTP version as the + incoming `Request` message and will only apply `HTTP/1.1` semantics if + applicable. This includes that the `Response` will automatically attach a + `Date` and `Connection: close` header if applicable. + + This implies that the `Server` now automatically responds with HTTP error + messages for invalid requests (status 400) and those exceeding internal + request header limits (status 431). + +## 0.5.0 (2017-02-16) + +* Feature / BC break: Change `Request` methods to be in line with PSR-7 + (#117 by @clue) + * Rename `getQuery()` to `getQueryParams()` + * Rename `getHttpVersion()` to `getProtocolVersion()` + * Change `getHeaders()` to always return an array of string values + for each header + +* Feature / BC break: Update Socket component to v0.5 and + add secure HTTPS server support + (#90 and #119 by @clue) + + ```php + // old plaintext HTTP server + $socket = new React\Socket\Server($loop); + $socket->listen(8080, '127.0.0.1'); + $http = new React\Http\Server($socket); + + // new plaintext HTTP server + $socket = new React\Socket\Server('127.0.0.1:8080', $loop); + $http = new React\Http\Server($socket); + + // new secure HTTPS server + $socket = new React\Socket\Server('127.0.0.1:8080', $loop); + $socket = new React\Socket\SecureServer($socket, $loop, array( + 'local_cert' => __DIR__ . '/localhost.pem' + )); + $http = new React\Http\Server($socket); + ``` + +* BC break: Mark internal APIs as internal or private and + remove unneeded `ServerInterface` + (#118 by @clue, #95 by @legionth) + +## 0.4.4 (2017-02-13) + +* Feature: Add request header accessors (à la PSR-7) + (#103 by @clue) + + ```php + // get value of host header + $host = $request->getHeaderLine('Host'); + + // get list of all cookie headers + $cookies = $request->getHeader('Cookie'); + ``` + +* Feature: Forward `pause()` and `resume()` from `Request` to underlying connection + (#110 by @clue) + + ```php + // support back-pressure when piping request into slower destination + $request->pipe($dest); + + // manually pause/resume request + $request->pause(); + $request->resume(); + ``` + +* Fix: Fix `100-continue` to be handled case-insensitive and ignore it for HTTP/1.0. + Similarly, outgoing response headers are now handled case-insensitive, e.g + we no longer apply chunked transfer encoding with mixed-case `Content-Length`. + (#107 by @clue) + + ```php + // now handled case-insensitive + $request->expectsContinue(); + + // now works just like properly-cased header + $response->writeHead($status, array('content-length' => 0)); + ``` + +* Fix: Do not emit empty `data` events and ignore empty writes in order to + not mess up chunked transfer encoding + (#108 and #112 by @clue) + +* Lock and test minimum required dependency versions and support PHPUnit v5 + (#113, #115 and #114 by @andig) + +## 0.4.3 (2017-02-10) + +* Fix: Do not take start of body into account when checking maximum header size + (#88 by @nopolabs) + +* Fix: Remove `data` listener if `HeaderParser` emits an error + (#83 by @nick4fake) + +* First class support for PHP 5.3 through PHP 7 and HHVM + (#101 and #102 by @clue, #66 by @WyriHaximus) + +* Improve test suite by adding PHPUnit to require-dev, + improving forward compatibility with newer PHPUnit versions + and replacing unneeded test stubs + (#92 and #93 by @nopolabs, #100 by @legionth) + +## 0.4.2 (2016-11-09) + +* Remove all listeners after emitting error in RequestHeaderParser #68 @WyriHaximus +* Catch Guzzle parse request errors #65 @WyriHaximus +* Remove branch-alias definition as per reactphp/react#343 #58 @WyriHaximus +* Add functional example to ease getting started #64 by @clue +* Naming, immutable array manipulation #37 @cboden + +## 0.4.1 (2015-05-21) + +* Replaced guzzle/parser with guzzlehttp/psr7 by @cboden +* FIX Continue Header by @iannsp +* Missing type hint by @marenzo + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* BC break: Update to Evenement 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 +* Bump React dependencies to v0.4 + +## 0.3.0 (2013-04-14) + +* Bump React dependencies to v0.3 + +## 0.2.6 (2012-12-26) + +* Bug fix: Emit end event when Response closes (@beaucollins) + +## 0.2.3 (2012-11-14) + +* Bug fix: Forward drain events from HTTP response (@cs278) +* Dependency: Updated guzzle deps to `3.0.*` + +## 0.2.2 (2012-10-28) + +* Version bump + +## 0.2.1 (2012-10-14) + +* Feature: Support HTTP 1.1 continue + +## 0.2.0 (2012-09-10) + +* Bump React dependencies to v0.2 + +## 0.1.1 (2012-07-12) + +* Version bump + +## 0.1.0 (2012-07-11) + +* First tagged release diff --git a/vendor/react/http/LICENSE b/vendor/react/http/LICENSE new file mode 100644 index 0000000..d6f8901 --- /dev/null +++ b/vendor/react/http/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/react/http/README.md b/vendor/react/http/README.md new file mode 100644 index 0000000..fd9ba46 --- /dev/null +++ b/vendor/react/http/README.md @@ -0,0 +1,3024 @@ +# HTTP + +[![CI status](https://github.com/reactphp/http/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/http/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/http?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/http) + +Event-driven, streaming HTTP client and server implementation for [ReactPHP](https://reactphp.org/). + +This HTTP library provides re-usable implementations for an HTTP client and +server based on ReactPHP's [`Socket`](https://github.com/reactphp/socket) and +[`EventLoop`](https://github.com/reactphp/event-loop) components. +Its client component allows you to send any number of async HTTP/HTTPS requests +concurrently. +Its server component allows you to build plaintext HTTP and secure HTTPS servers +that accept incoming HTTP requests from HTTP clients (such as web browsers). +This library provides async, streaming means for all of this, so you can handle +multiple concurrent HTTP requests without blocking. + +**Table of contents** + +* [Quickstart example](#quickstart-example) +* [Client Usage](#client-usage) + * [Request methods](#request-methods) + * [Promises](#promises) + * [Cancellation](#cancellation) + * [Timeouts](#timeouts) + * [Authentication](#authentication) + * [Redirects](#redirects) + * [Blocking](#blocking) + * [Concurrency](#concurrency) + * [Streaming response](#streaming-response) + * [Streaming request](#streaming-request) + * [HTTP proxy](#http-proxy) + * [SOCKS proxy](#socks-proxy) + * [SSH proxy](#ssh-proxy) + * [Unix domain sockets](#unix-domain-sockets) +* [Server Usage](#server-usage) + * [HttpServer](#httpserver) + * [listen()](#listen) + * [Server Request](#server-request) + * [Request parameters](#request-parameters) + * [Query parameters](#query-parameters) + * [Request body](#request-body) + * [Streaming incoming request](#streaming-incoming-request) + * [Request method](#request-method) + * [Cookie parameters](#cookie-parameters) + * [Invalid request](#invalid-request) + * [Server Response](#server-response) + * [Deferred response](#deferred-response) + * [Streaming outgoing response](#streaming-outgoing-response) + * [Response length](#response-length) + * [Invalid response](#invalid-response) + * [Default response headers](#default-response-headers) + * [Middleware](#middleware) + * [Custom middleware](#custom-middleware) + * [Third-Party Middleware](#third-party-middleware) +* [API](#api) + * [Browser](#browser) + * [get()](#get) + * [post()](#post) + * [head()](#head) + * [patch()](#patch) + * [put()](#put) + * [delete()](#delete) + * [request()](#request) + * [requestStreaming()](#requeststreaming) + * [withTimeout()](#withtimeout) + * [withFollowRedirects()](#withfollowredirects) + * [withRejectErrorResponse()](#withrejecterrorresponse) + * [withBase()](#withbase) + * [withProtocolVersion()](#withprotocolversion) + * [withResponseBuffer()](#withresponsebuffer) + * [withHeader()](#withheader) + * [withoutHeader()](#withoutheader) + * [React\Http\Message](#reacthttpmessage) + * [Response](#response) + * [html()](#html) + * [json()](#json) + * [plaintext()](#plaintext) + * [xml()](#xml) + * [Request](#request-1) + * [ServerRequest](#serverrequest) + * [Uri](#uri) + * [ResponseException](#responseexception) + * [React\Http\Middleware](#reacthttpmiddleware) + * [StreamingRequestMiddleware](#streamingrequestmiddleware) + * [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) + * [RequestBodyBufferMiddleware](#requestbodybuffermiddleware) + * [RequestBodyParserMiddleware](#requestbodyparsermiddleware) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Quickstart example + +Once [installed](#install), you can use the following code to access an +HTTP web server and send some simple HTTP GET requests: + +```php +get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +This is an HTTP server which responds with `Hello World!` to every request. + +```php +listen($socket); +``` + +See also the [examples](examples/). + +## Client Usage + +### Request methods + +Most importantly, this project provides a [`Browser`](#browser) object that +offers several methods that resemble the HTTP protocol methods: + +```php +$browser->get($url, array $headers = array()); +$browser->head($url, array $headers = array()); +$browser->post($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->delete($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->put($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +$browser->patch($url, array $headers = array(), string|ReadableStreamInterface $body = ''); +``` + +Each of these methods requires a `$url` and some optional parameters to send an +HTTP request. Each of these method names matches the respective HTTP request +method, for example the [`get()`](#get) method sends an HTTP `GET` request. + +You can optionally pass an associative array of additional `$headers` that will be +sent with this HTTP request. Additionally, each method will automatically add a +matching `Content-Length` request header if an outgoing request body is given and its +size 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` HTTP request methods). + +If you're using a [streaming request body](#streaming-request), it will default +to using `Transfer-Encoding: chunked` unless you explicitly pass in a matching `Content-Length` +request header. See also [streaming request](#streaming-request) for more details. + +By default, all of the above methods default to sending requests using the +HTTP/1.1 protocol version. If you want to explicitly use the legacy HTTP/1.0 +protocol version, you can use the [`withProtocolVersion()`](#withprotocolversion) +method. If you want to use any other or even custom HTTP request method, you can +use the [`request()`](#request) method. + +Each of the above methods supports async operation and either *fulfills* with a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +or *rejects* with an `Exception`. +Please see the following chapter about [promises](#promises) for more details. + +### Promises + +Sending requests is async (non-blocking), so you can actually send multiple +requests in parallel. +The `Browser` will respond to each request with a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +message, the order is not guaranteed. +Sending requests uses a [Promise](https://github.com/reactphp/promise)-based +interface that makes it easy to react to when an HTTP request is completed +(i.e. either successfully fulfilled or rejected with an error): + +```php +$browser->get($url)->then( + function (Psr\Http\Message\ResponseInterface $response) { + var_dump('Response received', $response); + }, + function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + } +); +``` + +If this looks strange to you, you can also use the more traditional [blocking API](#blocking). + +Keep in mind that resolving the Promise with the full response message means the +whole response body has to be kept in memory. +This is easy to get started and works reasonably well for smaller responses +(such as common HTML pages or RESTful or JSON API requests). + +You may also want to look into the [streaming API](#streaming-response): + +* If you're dealing with lots of concurrent requests (100+) or +* If you want to process individual data chunks as they happen (without having to wait for the full response body) or +* If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or +* If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). + +### Cancellation + +The returned Promise is implemented in such a way that it can be cancelled +when it is still pending. +Cancelling a pending promise will reject its value with an Exception and +clean up any underlying resources. + +```php +$promise = $browser->get($url); + +Loop::addTimer(2.0, function () use ($promise) { + $promise->cancel(); +}); +``` + +### Timeouts + +This library uses a very efficient HTTP implementation, so most HTTP requests +should usually be completed in mere milliseconds. However, when sending HTTP +requests over an unreliable network (the internet), there are a number of things +that can go wrong and may cause the request to fail after a time. As such, this +library respects PHP's `default_socket_timeout` setting (default 60s) as a timeout +for sending the outgoing HTTP request and waiting for a successful response and +will otherwise cancel the pending request and reject its value with an Exception. + +Note that this timeout value covers creating the underlying transport connection, +sending the HTTP request, receiving the HTTP response headers and its full +response body and following any eventual [redirects](#redirects). See also +[redirects](#redirects) below to configure the number of redirects to follow (or +disable following redirects altogether) and also [streaming](#streaming-response) +below to not take receiving large response bodies into account for this timeout. + +You can use the [`withTimeout()` method](#withtimeout) to pass a custom timeout +value in seconds like this: + +```php +$browser = $browser->withTimeout(10.0); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // response received within 10 seconds maximum + var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Similarly, you can use a bool `false` to not apply a timeout at all +or use a bool `true` value to restore the default handling. +See [`withTimeout()`](#withtimeout) for more details. + +If you're using a [streaming response body](#streaming-response), the time it +takes to receive the response body stream will not be included in the timeout. +This allows you to keep this incoming stream open for a longer time, such as +when downloading a very large stream or when streaming data over a long-lived +connection. + +If you're using a [streaming request body](#streaming-request), the time it +takes to send the request body stream will not be included in the timeout. This +allows you to keep this outgoing stream open for a longer time, such as when +uploading a very large stream. + +Note that this timeout handling applies to the higher-level HTTP layer. Lower +layers such as socket and DNS may also apply (different) timeout values. In +particular, the underlying socket connection uses the same `default_socket_timeout` +setting to establish the underlying transport connection. To control this +connection timeout behavior, you can [inject a custom `Connector`](#browser) +like this: + +```php +$browser = new React\Http\Browser( + new React\Socket\Connector( + array( + 'timeout' => 5 + ) + ) +); +``` + +### Authentication + +This library supports [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) +using the `Authorization: Basic …` request header or allows you to set an explicit +`Authorization` request header. + +By default, this library does not include an outgoing `Authorization` request +header. If the server requires authentication, if may return a `401` (Unauthorized) +status code which will reject the request by default (see also the +[`withRejectErrorResponse()` method](#withrejecterrorresponse) below). + +In order to pass authentication details, you can simply pass the username and +password as part of the request URL like this: + +```php +$promise = $browser->get('https://user:pass@example.com/api'); +``` + +Note that special characters in the authentication details have to be +percent-encoded, see also [`rawurlencode()`](https://www.php.net/manual/en/function.rawurlencode.php). +This example will automatically pass the base64-encoded authentication details +using the outgoing `Authorization: Basic …` request header. If the HTTP endpoint +you're talking to requires any other authentication scheme, you can also pass +this header explicitly. This is common when using (RESTful) HTTP APIs that use +OAuth access tokens or JSON Web Tokens (JWT): + +```php +$token = 'abc123'; + +$promise = $browser->get( + 'https://example.com/api', + array( + 'Authorization' => 'Bearer ' . $token + ) +); +``` + +When following redirects, the `Authorization` request header will never be sent +to any remote hosts by default. When following a redirect where the `Location` +response header contains authentication details, these details will be sent for +following requests. See also [redirects](#redirects) below. + +### Redirects + +By default, this library follows any redirects and obeys `3xx` (Redirection) +status codes using the `Location` response header from the remote server. +The promise will be fulfilled with the last response from the chain of redirects. + +```php +$browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) { + // the final response will end up here + var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Any redirected requests will follow the semantics of the original request and +will include the same request headers as the original request except for those +listed below. +If the original request is a temporary (307) or a permanent (308) redirect, request +body and headers will be passed to the redirected request. Otherwise, the request +body will never be passed to the redirected request. Accordingly, each redirected +request will remove any `Content-Length` and `Content-Type` request headers. + +If the original request used HTTP authentication with an `Authorization` request +header, this request header will only be passed as part of the redirected +request if the redirected URL is using the same host. In other words, the +`Authorizaton` request header will not be forwarded to other foreign hosts due to +possible privacy/security concerns. When following a redirect where the `Location` +response header contains authentication details, these details will be sent for +following requests. + +You can use the [`withFollowRedirects()`](#withfollowredirects) method to +control the maximum number of redirects to follow or to return any redirect +responses as-is and apply custom redirection logic like this: + +```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->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also [`withFollowRedirects()`](#withfollowredirects) for more details. + +### Blocking + +As stated above, this library provides you a powerful, async API by default. + +You can also integrate this into your traditional, blocking environment by using +[reactphp/async](https://github.com/reactphp/async). This allows you to simply +await async HTTP requests like this: + +```php +use function React\Async\await; + +$browser = new React\Http\Browser(); + +$promise = $browser->get('http://example.com/'); + +try { + $response = await($promise); + // response successfully received +} catch (Exception $e) { + // an error occurred while performing the request +} +``` + +Similarly, you can also process multiple requests concurrently and await an array of `Response` objects: + +```php +use function React\Async\await; +use function React\Promise\all; + +$promises = array( + $browser->get('http://example.com/'), + $browser->get('http://www.example.org/'), +); + +$responses = await(all($promises)); +``` + +This is made possible thanks to fibers available in PHP 8.1+ and our +compatibility API that also works on all supported PHP versions. +Please refer to [reactphp/async](https://github.com/reactphp/async#readme) for more details. + +Keep in mind the above remark about buffering the whole response message in memory. +As an alternative, you may also see one of the following chapters for the +[streaming API](#streaming-response). + +### Concurrency + +As stated above, this library provides you a powerful, async API. Being able to +send a large number of requests at once is one of the core features of this +project. For instance, you can easily send 100 requests concurrently while +processing SQL queries at the same time. + +Remember, with great power comes great responsibility. Sending an excessive +number of requests may either take up all resources on your side or it may even +get you banned by the remote side if it sees an unreasonable number of requests +from your side. + +```php +// watch out if array contains many elements +foreach ($urls as $url) { + $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); + }, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + }); +} +``` + +As a consequence, it's usually recommended to limit concurrency on the sending +side to a reasonable value. It's common to use a rather small limit, as doing +more than a dozen of things at once may easily overwhelm the receiving side. You +can use [clue/reactphp-mq](https://github.com/clue/reactphp-mq) as a lightweight +in-memory queue to concurrently do many (but not too many) things at once: + +```php +// wraps Browser in a Queue object that executes no more than 10 operations at once +$q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) { + return $browser->get($url); +}); + +foreach ($urls as $url) { + $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); + }, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + }); +} +``` + +Additional requests that exceed the concurrency limit will automatically be +enqueued until one of the pending requests completes. This integrates nicely +with the existing [Promise-based API](#promises). Please refer to +[clue/reactphp-mq](https://github.com/clue/reactphp-mq) for more details. + +This in-memory approach works reasonably well for some thousand outstanding +requests. If you're processing a very large input list (think millions of rows +in a CSV or NDJSON file), you may want to look into using a streaming approach +instead. See [clue/reactphp-flux](https://github.com/clue/reactphp-flux) for +more details. + +### Streaming response + +All of the above examples assume you want to store the whole response body in memory. +This is easy to get started and works reasonably well for smaller responses. + +However, there are several situations where it's usually a better idea to use a +streaming approach, where only small chunks have to be kept in memory: + +* If you're dealing with lots of concurrent requests (100+) or +* If you want to process individual data chunks as they happen (without having to wait for the full response body) or +* If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or +* If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). + +You can use the [`requestStreaming()`](#requeststreaming) 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. + +This means it resolves with a normal +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), +which can be used to access the response message parameters as usual. +You can access the message body as usual, however it now also +implements [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +as well as parts of the [PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface). + +```php +$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + $body = $response->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + $body->on('data', function ($chunk) { + echo $chunk; + }); + + $body->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + }); + + $body->on('close', function () { + echo '[DONE]' . PHP_EOL; + }); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also the [stream download benchmark example](examples/91-client-benchmark-download.php) and +the [stream forwarding example](examples/21-client-request-streaming-to-stdout.php). + +You can invoke the following methods on the message body: + +```php +$body->on($event, $callback); +$body->eof(); +$body->isReadable(); +$body->pipe(React\Stream\WritableStreamInterface $dest, array $options = array()); +$body->close(); +$body->pause(); +$body->resume(); +``` + +Because the message body is in a streaming state, invoking the following methods +doesn't make much sense: + +```php +$body->__toString(); // '' +$body->detach(); // throws BadMethodCallException +$body->getSize(); // null +$body->tell(); // throws BadMethodCallException +$body->isSeekable(); // false +$body->seek(); // throws BadMethodCallException +$body->rewind(); // throws BadMethodCallException +$body->isWritable(); // false +$body->write(); // throws BadMethodCallException +$body->read(); // throws BadMethodCallException +$body->getContents(); // throws BadMethodCallException +``` + +Note how [timeouts](#timeouts) apply slightly differently when using streaming. +In streaming mode, the timeout value covers creating the underlying transport +connection, sending the HTTP request, receiving the HTTP response headers and +following any eventual [redirects](#redirects). In particular, the timeout value +does not take receiving (possibly large) response bodies into account. + +If you want to integrate the streaming response into a higher level API, then +working with Promise objects that resolve with Stream objects is often inconvenient. +Consider looking into also using [react/promise-stream](https://github.com/reactphp/promise-stream). +The resulting streaming code could look something like this: + +```php +use React\Promise\Stream; + +function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface { + return Stream\unwrapReadable( + $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + return $response->getBody(); + }) + ); +} + +$stream = download($browser, $url); +$stream->on('data', function ($data) { + echo $data; +}); +$stream->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also the [`requestStreaming()`](#requeststreaming) method for more details. + +### Streaming request + +Besides streaming the response body, you can also stream the request body. +This can be useful if you want to send big POST requests (uploading files etc.) +or process many outgoing streams at once. +Instead of passing the body as a string, you can simply pass an instance +implementing [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +to the [request methods](#request-methods) like this: + +```php +$browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { + echo 'Successfully sent.'; +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +If you're using a streaming request body (`React\Stream\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); +``` + +If the streaming request body emits an `error` event or is explicitly closed +without emitting a successful `end` event first, the request will automatically +be closed and rejected. + +### HTTP proxy + +You can also establish your outgoing connections through an HTTP CONNECT proxy server +by adding a dependency to [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-proxy). + +HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy") +are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to +conceal the origin address (anonymity) or to circumvent address blocking +(geoblocking). While many (public) HTTP CONNECT proxy servers often limit this +to HTTPS port `443` only, this can technically be used to tunnel any TCP/IP-based +protocol, such as plain HTTP and TLS-encrypted HTTPS. + +```php +$proxy = new Clue\React\HttpProxy\ProxyConnector('127.0.0.1:8080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($connector); +``` + +See also the [HTTP proxy example](examples/11-client-http-proxy.php). + +### SOCKS proxy + +You can also establish your outgoing connections through a SOCKS proxy server +by adding a dependency to [clue/reactphp-socks](https://github.com/clue/reactphp-socks). + +The SOCKS proxy protocol family (SOCKS5, SOCKS4 and SOCKS4a) is commonly used to +tunnel HTTP(S) traffic through an intermediary ("proxy"), to conceal the origin +address (anonymity) or to circumvent address blocking (geoblocking). While many +(public) SOCKS proxy servers often limit this to HTTP(S) port `80` and `443` +only, this can technically be used to tunnel any TCP/IP-based protocol. + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($connector); +``` + +See also the [SOCKS proxy example](examples/12-client-socks-proxy.php). + +### SSH proxy + +You can also establish your outgoing connections through an SSH server +by adding a dependency to [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy). + +[Secure Shell (SSH)](https://en.wikipedia.org/wiki/Secure_Shell) is a secure +network protocol that is most commonly used to access a login shell on a remote +server. Its architecture allows it to use multiple secure channels over a single +connection. Among others, this can also be used to create an "SSH tunnel", which +is commonly used to tunnel HTTP(S) traffic through an intermediary ("proxy"), to +conceal the origin address (anonymity) or to circumvent address blocking +(geoblocking). This can be used to tunnel any TCP/IP-based protocol (HTTP, SMTP, +IMAP etc.), allows you to access local services that are otherwise not accessible +from the outside (database behind firewall) and as such can also be used for +plain HTTP and TLS-encrypted HTTPS. + +```php +$proxy = new Clue\React\SshProxy\SshSocksConnector('alice@example.com'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($connector); +``` + +See also the [SSH proxy example](examples/13-client-ssh-proxy.php). + +### Unix domain sockets + +By default, this library supports transport over plaintext TCP/IP and secure +TLS connections for the `http://` and `https://` URL schemes respectively. +This library also supports Unix domain sockets (UDS) when explicitly configured. + +In order to use a UDS path, you have to explicitly configure the connector to +override the destination URL so that the hostname given in the request URL will +no longer be used to establish the connection: + +```php +$connector = new React\Socket\FixedUriConnector( + 'unix:///var/run/docker.sock', + new React\Socket\UnixConnector() +); + +$browser = new React\Http\Browser($connector); + +$client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also the [Unix Domain Sockets (UDS) example](examples/14-client-unix-domain-sockets.php). + + +## Server Usage + +### HttpServer + + + +The `React\Http\HttpServer` class is responsible for handling incoming connections and then +processing each incoming HTTP request. + +When a complete HTTP request has been received, it will invoke the given +request handler function. This request handler function needs to be passed to +the constructor and will be invoked with the respective [request](#server-request) +object and expects a [response](#server-response) object in return: + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" + ); +}); +``` + +Each incoming HTTP request message is always represented by the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), +see also following [request](#server-request) chapter for more details. + +Each outgoing HTTP response message is always represented by the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), +see also following [response](#server-response) chapter for more details. + +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +In order to start listening for any incoming connections, the `HttpServer` needs +to be attached to an instance of +[`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) +through the [`listen()`](#listen) method as described in the following +chapter. In its most simple form, you can attach this to a +[`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +in order to start a plaintext HTTP server like this: + +```php +$http = new React\Http\HttpServer($handler); + +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); +$http->listen($socket); +``` + +See also the [`listen()`](#listen) method and the +[hello world server example](examples/51-server-hello-world.php) +for more details. + +By default, the `HttpServer` buffers and parses the complete incoming HTTP +request in memory. It will invoke the given request handler function when the +complete request headers and request body has been received. This means the +[request](#server-request) object passed to your request handler function will be +fully compatible with PSR-7 (http-message). This provides sane defaults for +80% of the use cases and is the recommended way to use this library unless +you're sure you know what you're doing. + +On the other hand, buffering complete HTTP requests in memory until they can +be processed by your request handler function means that this class has to +employ a number of limits to avoid consuming too much memory. In order to +take the more advanced configuration out your hand, it respects setting from +your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its +default settings. This is a list of PHP settings this class respects with +their respective default values: + +``` +memory_limit 128M +post_max_size 8M // capped at 64K + +enable_post_data_reading 1 +max_input_nesting_level 64 +max_input_vars 1000 + +file_uploads 1 +upload_max_filesize 2M +max_file_uploads 20 +``` + +In particular, the `post_max_size` setting limits how much memory a single +HTTP request is allowed to consume while buffering its request body. This +needs to be limited because the server can process a large number of requests +concurrently, so the server may potentially consume a large amount of memory +otherwise. To support higher concurrency by default, this value is capped +at `64K`. If you assign a higher value, it will only allow `64K` by default. +If a request exceeds this limit, its request body will be ignored and it will +be processed like a request with no request body at all. See below for +explicit configuration to override this setting. + +By default, this class will try to avoid consuming more than half of your +`memory_limit` for buffering multiple concurrent HTTP requests. As such, with +the above default settings of `128M` max, it will try to consume no more than +`64M` for buffering multiple concurrent HTTP requests. As a consequence, it +will limit the concurrency to `1024` HTTP requests with the above defaults. + +It is imperative that you assign reasonable values to your PHP ini settings. +It is usually recommended to not support buffering incoming HTTP requests +with a large HTTP request body (e.g. large file uploads). If you want to +increase this buffer size, you will have to also increase the total memory +limit to allow for more concurrent requests (set `memory_limit 512M` or more) +or explicitly limit concurrency. + +In order to override the above buffering defaults, you can configure the +`HttpServer` explicitly. You can use the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to explicitly configure the total number of requests that can be handled at +once like this: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +); +``` + +In this example, we allow processing up to 100 concurrent requests at once +and each request can buffer up to `2M`. This means you may have to keep a +maximum of `200M` of memory for incoming request body buffers. Accordingly, +you need to adjust the `memory_limit` ini setting to allow for these buffers +plus your actual application logic memory requirements (think `512M` or more). + +> Internally, this class automatically assigns these middleware handlers + automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + is given. Accordingly, you can use this example to override all default + settings to implement custom limits. + +As an alternative to buffering the complete request body in memory, you can +also use a streaming approach where only small chunks of data have to be kept +in memory: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + $handler +); +``` + +In this case, it will invoke the request handler function once the HTTP +request headers have been received, i.e. before receiving the potentially +much larger HTTP request body. This means the [request](#server-request) passed to +your request handler function may not be fully compatible with PSR-7. This is +specifically designed to help with more advanced use cases where you want to +have full control over consuming the incoming HTTP request body and +concurrency settings. See also [streaming incoming request](#streaming-incoming-request) +below for more details. + +> Changelog v1.5.0: This class has been renamed to `HttpServer` from the + previous `Server` class in order to avoid any ambiguities. + The previous name has been deprecated and should not be used anymore. + +### listen() + +The `listen(React\Socket\ServerInterface $socket): void` method can be used to +start listening for HTTP requests on the given socket server instance. + +The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) +is responsible for emitting the underlying streaming connections. This +HTTP server needs to be attached to it in order to process any +connections and pase incoming streaming data as incoming HTTP request +messages. In its most common form, you can attach this to a +[`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +in order to start a plaintext HTTP server like this: + +```php +$http = new React\Http\HttpServer($handler); + +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); +$http->listen($socket); +``` + +See also [hello world server example](examples/51-server-hello-world.php) +for more details. + +This example will start listening for HTTP requests on the alternative +HTTP port `8080` on all interfaces (publicly). As an alternative, it is +very common to use a reverse proxy and let this HTTP server listen on the +localhost (loopback) interface only by using the listen address +`127.0.0.1:8080` instead. This way, you host your application(s) on the +default HTTP port `80` and only route specific requests to this HTTP +server. + +Likewise, it's usually recommended to use a reverse proxy setup to accept +secure HTTPS requests on default HTTPS port `443` (TLS termination) and +only route plaintext requests to this HTTP server. As an alternative, you +can also accept secure HTTPS requests with this HTTP server by attaching +this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) +using a secure TLS listen address, a certificate file and optional +`passphrase` like this: + +```php +$http = new React\Http\HttpServer($handler); + +$socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + 'tls' => array( + 'local_cert' => __DIR__ . '/localhost.pem' + ) +)); +$http->listen($socket); +``` + +See also [hello world HTTPS example](examples/61-server-hello-world-https.php) +for more details. + +### Server Request + +As seen above, the [`HttpServer`](#httpserver) class is responsible for handling +incoming connections and then processing each incoming HTTP request. + +The request object will be processed once the request has +been received by the client. +This request object implements the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) +which in turn extends the +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) +and will be passed to the callback function like this. + + ```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $body = "The method of the request is: " . $request->getMethod() . "\n"; + $body .= "The requested path is: " . $request->getUri()->getPath() . "\n"; + + return React\Http\Message\Response::plaintext( + $body + ); +}); +``` + +For more details about the request object, also check out the documentation of +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) +and +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface). + +#### Request parameters + +The `getServerParams(): mixed[]` method can be used to +get server-side parameters similar to the `$_SERVER` variable. +The following parameters are currently available: + +* `REMOTE_ADDR` + The IP address of the request sender +* `REMOTE_PORT` + Port of the request sender +* `SERVER_ADDR` + The IP address of the server +* `SERVER_PORT` + The port of the server +* `REQUEST_TIME` + Unix timestamp when the complete request header has been received, + as integer similar to `time()` +* `REQUEST_TIME_FLOAT` + Unix timestamp when the complete request header has been received, + as float similar to `microtime(true)` +* `HTTPS` + Set to 'on' if the request used HTTPS, otherwise it won't be set + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $body = "Your IP is: " . $request->getServerParams()['REMOTE_ADDR'] . "\n"; + + return React\Http\Message\Response::plaintext( + $body + ); +}); +``` + +See also [whatsmyip server example](examples/53-server-whatsmyip.php). + +> Advanced: Note that address parameters will not be set if you're listening on + a Unix domain socket (UDS) path as this protocol lacks the concept of + host/port. + +#### Query parameters + +The `getQueryParams(): array` method can be used to get the query parameters +similiar to the `$_GET` variable. + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $queryParams = $request->getQueryParams(); + + $body = 'The query parameter "foo" is not set. Click the following link '; + $body .= 'to use query parameter in your request'; + + if (isset($queryParams['foo'])) { + $body = 'The value of "foo" is: ' . htmlspecialchars($queryParams['foo']); + } + + return React\Http\Message\Response::html( + $body + ); +}); +``` + +The response in the above example will return a response body with a link. +The URL contains the query parameter `foo` with the value `bar`. +Use [`htmlentities`](https://www.php.net/manual/en/function.htmlentities.php) +like in this example to prevent +[Cross-Site Scripting (abbreviated as XSS)](https://en.wikipedia.org/wiki/Cross-site_scripting). + +See also [server query parameters example](examples/54-server-query-parameter.php). + +#### Request body + +By default, the [`Server`](#httpserver) will buffer and parse the full request body +in memory. This means the given request object includes the parsed request body +and any file uploads. + +> As an alternative to the default buffering logic, you can also use the + [`StreamingRequestMiddleware`](#streamingrequestmiddleware). Jump to the next + chapter to learn more about how to process a + [streaming incoming request](#streaming-incoming-request). + +As stated above, each incoming HTTP request is always represented by the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface). +This interface provides several methods that are useful when working with the +incoming request body as described below. + +The `getParsedBody(): null|array|object` method can be used to +get the parsed request body, similar to +[PHP's `$_POST` variable](https://www.php.net/manual/en/reserved.variables.post.php). +This method may return a (possibly nested) array structure with all body +parameters or a `null` value if the request body could not be parsed. +By default, this method will only return parsed data for requests using +`Content-Type: application/x-www-form-urlencoded` or `Content-Type: multipart/form-data` +request headers (commonly used for `POST` requests for HTML form submission data). + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $name = $request->getParsedBody()['name'] ?? 'anonymous'; + + return React\Http\Message\Response::plaintext( + "Hello $name!\n" + ); +}); +``` + +See also [form upload example](examples/62-server-form-upload.php) for more details. + +The `getBody(): StreamInterface` method can be used to +get the raw data from this request body, similar to +[PHP's `php://input` stream](https://www.php.net/manual/en/wrappers.php.php#wrappers.php.input). +This method returns an instance of the request body represented by the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface). +This is particularly useful when using a custom request body that will not +otherwise be parsed by default, such as a JSON (`Content-Type: application/json`) or +an XML (`Content-Type: application/xml`) request body (which is commonly used for +`POST`, `PUT` or `PATCH` requests in JSON-based or RESTful/RESTish APIs). + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $data = json_decode((string)$request->getBody()); + $name = $data->name ?? 'anonymous'; + + return React\Http\Message\Response::json( + ['message' => "Hello $name!"] + ); +}); +``` + +See also [JSON API server example](examples/59-server-json-api.php) for more details. + +The `getUploadedFiles(): array` method can be used to +get the uploaded files in this request, similar to +[PHP's `$_FILES` variable](https://www.php.net/manual/en/reserved.variables.files.php). +This method returns a (possibly nested) array structure with all file uploads, each represented by the +[PSR-7 `UploadedFileInterface`](https://www.php-fig.org/psr/psr-7/#36-psrhttpmessageuploadedfileinterface). +This array will only be filled when using the `Content-Type: multipart/form-data` +request header (commonly used for `POST` requests for HTML file uploads). + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $files = $request->getUploadedFiles(); + $name = isset($files['avatar']) ? $files['avatar']->getClientFilename() : 'nothing'; + + return React\Http\Message\Response::plaintext( + "Uploaded $name\n" + ); +}); +``` + +See also [form upload server example](examples/62-server-form-upload.php) for more details. + +The `getSize(): ?int` method can be used to +get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. +This method returns the complete size of the request body measured in number +of bytes as defined by the message boundaries. +This value may be `0` if the request message does not contain a request body +(such as a simple `GET` request). +This method operates on the buffered request body, i.e. the request body size +is always known, even when the request does not specify a `Content-Length` request +header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. + +> Note: The `HttpServer` automatically takes care of handling requests with the + additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to + send a bigger request body, they MAY send only the request headers with an + additional `Expect: 100-continue` request header and wait before sending the actual + (large) message body. In this case the server will automatically send an + intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you + will receive the request body without a delay as expected. + +#### Streaming incoming request + +If you're using the advanced [`StreamingRequestMiddleware`](#streamingrequestmiddleware), +the request object will be processed once the request headers have been received. +This means that this happens irrespective of (i.e. *before*) receiving the +(potentially much larger) request body. + +> Note that this is non-standard behavior considered advanced usage. Jump to the + previous chapter to learn more about how to process a buffered [request body](#request-body). + +While this may be uncommon in the PHP ecosystem, this is actually a very powerful +approach that gives you several advantages not otherwise possible: + +* React to requests *before* receiving a large request body, + such as rejecting an unauthenticated request or one that exceeds allowed + message lengths (file uploads). +* Start processing parts of the request body before the remainder of the request + body arrives or if the sender is slowly streaming data. +* Process a large request body without having to buffer anything in memory, + such as accepting a huge file upload or possibly unlimited request body stream. + +The `getBody(): StreamInterface` method can be used to +access the request body stream. +In the streaming mode, this method returns a stream instance that implements both the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +and the [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). +However, most of the +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +methods have been designed under the assumption of being in control of a +synchronous request body. +Given that this does not apply to this server, the following +[PSR-7 `StreamInterface`](https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface) +methods are not used and SHOULD NOT be called: +`tell()`, `eof()`, `seek()`, `rewind()`, `write()` and `read()`. +If this is an issue for your use case and/or you want to access uploaded files, +it's highly recommended to use a buffered [request body](#request-body) or use the +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) instead. +The [ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +gives you access to the incoming request body as the individual chunks arrive: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + return new React\Promise\Promise(function ($resolve, $reject) use ($body) { + $bytes = 0; + $body->on('data', function ($data) use (&$bytes) { + $bytes += strlen($data); + }); + + $body->on('end', function () use ($resolve, &$bytes){ + $resolve(React\Http\Message\Response::plaintext( + "Received $bytes bytes\n" + )); + }); + + // an error occures e.g. on invalid chunked encoded data or an unexpected 'end' event + $body->on('error', function (Exception $e) use ($resolve, &$bytes) { + $resolve(React\Http\Message\Response::plaintext( + "Encountered error after $bytes bytes: {$e->getMessage()}\n" + )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST)); + }); + }); + } +); +``` + +The above example simply counts the number of bytes received in the request body. +This can be used as a skeleton for buffering or processing the request body. + +See also [streaming request server example](examples/63-server-streaming-request.php) for more details. + +The `data` event will be emitted whenever new data is available on the request +body stream. +The server also automatically takes care of decoding any incoming requests using +`Transfer-Encoding: chunked` and will only emit the actual payload as data. + +The `end` event will be emitted when the request body stream terminates +successfully, i.e. it was read until its expected end. + +The `error` event will be emitted in case the request stream contains invalid +data for `Transfer-Encoding: chunked` or when the connection closes before +the complete request stream has been received. +The server will automatically stop reading from the connection and discard all +incoming data instead of closing it. +A response message can still be sent (unless the connection is already closed). + +A `close` event will be emitted after an `error` or `end` event. + +For more details about the request body stream, check out the documentation of +[ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface). + +The `getSize(): ?int` method can be used to +get the size of the request body, similar to PHP's `$_SERVER['CONTENT_LENGTH']` variable. +This method returns the complete size of the request body measured in number +of bytes as defined by the message boundaries. +This value may be `0` if the request message does not contain a request body +(such as a simple `GET` request). +This method operates on the streaming request body, i.e. the request body size +may be unknown (`null`) when using `Transfer-Encoding: chunked` for HTTP/1.1 requests. + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $size = $request->getBody()->getSize(); + if ($size === null) { + $body = "The request does not contain an explicit length. "; + $body .= "This example does not accept chunked transfer encoding.\n"; + + return React\Http\Message\Response::plaintext( + $body + )->withStatus(React\Http\Message\Response::STATUS_LENGTH_REQUIRED); + } + + return React\Http\Message\Response::plaintext( + "Request body size: " . $size . " bytes\n" + ); + } +); +``` + +> Note: The `HttpServer` automatically takes care of handling requests with the + additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to + send a bigger request body, they MAY send only the request headers with an + additional `Expect: 100-continue` request header and wait before sending the actual + (large) message body. In this case the server will automatically send an + intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you + will receive the streaming request body without a delay as expected. + +#### Request method + +Note that the server supports *any* request method (including custom and non- +standard ones) and all request-target formats defined in the HTTP specs for each +respective method, including *normal* `origin-form` requests as well as +proxy requests in `absolute-form` and `authority-form`. +The `getUri(): UriInterface` method can be used to get the effective request +URI which provides you access to individiual URI components. +Note that (depending on the given `request-target`) certain URI components may +or may not be present, for example the `getPath(): string` method will return +an empty string for requests in `asterisk-form` or `authority-form`. +Its `getHost(): string` method will return the host as determined by the +effective request URI, which defaults to the local socket address if an HTTP/1.0 +client did not specify one (i.e. no `Host` header). +Its `getScheme(): string` method will return `http` or `https` depending +on whether the request was made over a secure TLS connection to the target host. + +The `Host` header value will be sanitized to match this host component plus the +port component only if it is non-standard for this URI scheme. + +You can use `getMethod(): string` and `getRequestTarget(): string` to +check this is an accepted request and may want to reject other requests with +an appropriate error code, such as `400` (Bad Request) or `405` (Method Not +Allowed). + +> The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not + something most HTTP servers would want to care about. + Note that if you want to handle this method, the client MAY send a different + request-target than the `Host` header value (such as removing default ports) + and the request-target MUST take precendence when forwarding. + +#### Cookie parameters + +The `getCookieParams(): string[]` method can be used to +get all cookies sent with the current request. + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $key = 'greeting'; + + if (isset($request->getCookieParams()[$key])) { + $body = "Your cookie value is: " . $request->getCookieParams()[$key] . "\n"; + + return React\Http\Message\Response::plaintext( + $body + ); + } + + return React\Http\Message\Response::plaintext( + "Your cookie has been set.\n" + )->withHeader('Set-Cookie', $key . '=' . urlencode('Hello world!')); +}); +``` + +The above example will try to set a cookie on first access and +will try to print the cookie value on all subsequent tries. +Note how the example uses the `urlencode()` function to encode +non-alphanumeric characters. +This encoding is also used internally when decoding the name and value of cookies +(which is in line with other implementations, such as PHP's cookie functions). + +See also [cookie server example](examples/55-server-cookie-handling.php) for more details. + +#### Invalid request + +The `HttpServer` class supports both HTTP/1.1 and HTTP/1.0 request messages. +If a client sends an invalid request message, uses an invalid HTTP +protocol version or sends an invalid `Transfer-Encoding` request header value, +the server will automatically send a `400` (Bad Request) HTTP error response +to the client and close the connection. +On top of this, it will emit an `error` event that can be used for logging +purposes like this: + +```php +$http->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that the server will also emit an `error` event if you do not return a +valid response object from your request handler function. See also +[invalid response](#invalid-response) for more details. + +### Server Response + +The callback function passed to the constructor of the [`HttpServer`](#httpserver) is +responsible for processing the request and returning a response, which will be +delivered to the client. + +This function MUST return an instance implementing +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +object or a +[ReactPHP Promise](https://github.com/reactphp/promise) +which resolves with a [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) object. + +This projects ships a [`Response` class](#response) which implements the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). +In its most simple form, you can use it like this: + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" + ); +}); +``` + +We use this [`Response` class](#response) throughout our project examples, but +feel free to use any other implementation of the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). +See also the [`Response` class](#response) for more details. + +#### Deferred response + +The example above returns the response directly, because it needs +no time to be processed. +Using a database, the file system or long calculations +(in fact every action that will take >=1ms) to create your +response, will slow down the server. +To prevent this you SHOULD use a +[ReactPHP Promise](https://github.com/reactphp/promise#reactpromise). +This example shows how such a long-term action could look like: + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $promise = new Promise(function ($resolve, $reject) { + Loop::addTimer(1.5, function() use ($resolve) { + $resolve(); + }); + }); + + return $promise->then(function () { + return React\Http\Message\Response::plaintext( + "Hello World!" + ); + }); +}); +``` + +The above example will create a response after 1.5 second. +This example shows that you need a promise, +if your response needs time to created. +The `ReactPHP Promise` will resolve in a `Response` object when the request +body ends. +If the client closes the connection while the promise is still pending, the +promise will automatically be cancelled. +The promise cancellation handler can be used to clean up any pending resources +allocated in this case (if applicable). +If a promise is resolved after the client closes, it will simply be ignored. + +#### Streaming outgoing response + +The `Response` class in this project supports to add an instance which implements the +[ReactPHP `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +for the response body. +So you are able stream data directly into the response body. +Note that other implementations of the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +may only support strings. + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $stream = new ThroughStream(); + + // send some data every once in a while with periodic timer + $timer = Loop::addPeriodicTimer(0.5, function () use ($stream) { + $stream->write(microtime(true) . PHP_EOL); + }); + + // end stream after a few seconds + $timeout = Loop::addTimer(5.0, function() use ($stream, $timer) { + Loop::cancelTimer($timer); + $stream->end(); + }); + + // stop timer if stream is closed (such as when connection is closed) + $stream->on('close', function () use ($timer, $timeout) { + Loop::cancelTimer($timer); + Loop::cancelTimer($timeout); + }); + + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Content-Type' => 'text/plain' + ), + $stream + ); +}); +``` + +The above example will emit every 0.5 seconds the current Unix timestamp +with microseconds as float to the client and will end after 5 seconds. +This is just a example you could use of the streaming, +you could also send a big amount of data via little chunks +or use it for body data that needs to calculated. + +If the request handler resolves with a response stream that is already closed, +it will simply send an empty response body. +If the client closes the connection while the stream is still open, the +response stream will automatically be closed. +If a promise is resolved with a streaming body after the client closes, the +response stream will automatically be closed. +The `close` event can be used to clean up any pending resources allocated +in this case (if applicable). + +> Note that special care has to be taken if you use a body stream instance that + implements ReactPHP's + [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) + (such as the `ThroughStream` in the above example). +> +> For *most* cases, this will simply only consume its readable side and forward + (send) any data that is emitted by the stream, thus entirely ignoring the + writable side of the stream. + If however this is either a `101` (Switching Protocols) response or a `2xx` + (Successful) response to a `CONNECT` method, it will also *write* data to the + writable side of the stream. + This can be avoided by either rejecting all requests with the `CONNECT` + method (which is what most *normal* origin HTTP servers would likely do) or + or ensuring that only ever an instance of + [ReactPHP's `ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + is used. +> +> The `101` (Switching Protocols) response code is useful for the more advanced + `Upgrade` requests, such as upgrading to the WebSocket protocol or + implementing custom protocol logic that is out of scope of the HTTP specs and + this HTTP library. + If you want to handle the `Upgrade: WebSocket` header, you will likely want + to look into using [Ratchet](http://socketo.me/) instead. + If you want to handle a custom protocol, you will likely want to look into the + [HTTP specs](https://tools.ietf.org/html/rfc7230#section-6.7) and also see + [examples #81 and #82](examples/) for more details. + In particular, the `101` (Switching Protocols) response code MUST NOT be used + unless you send an `Upgrade` response header value that is also present in + the corresponding HTTP/1.1 `Upgrade` request header value. + The server automatically takes care of sending a `Connection: upgrade` + header value in this case, so you don't have to. +> +> The `CONNECT` method is useful in a tunneling setup (HTTPS proxy) and not + something most origin HTTP servers would want to care about. + The HTTP specs define an opaque "tunneling mode" for this method and make no + use of the message body. + For consistency reasons, this library uses a `DuplexStreamInterface` in the + response body for tunneled application data. + This implies that that a `2xx` (Successful) response to a `CONNECT` request + can in fact use a streaming response body for the tunneled application data, + so that any raw data the client sends over the connection will be piped + through the writable stream for consumption. + Note that while the HTTP specs make no use of the request body for `CONNECT` + requests, one may still be present. Normal request body processing applies + here and the connection will only turn to "tunneling mode" after the request + body has been processed (which should be empty in most cases). + See also [HTTP CONNECT server example](examples/72-server-http-connect-proxy.php) for more details. + +#### Response length + +If the response body size is known, a `Content-Length` response header will be +added automatically. This is the most common use case, for example when using +a `string` response body like this: + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + return React\Http\Message\Response::plaintext( + "Hello World!\n" + ); +}); +``` + +If the response body size is unknown, a `Content-Length` response header can not +be added automatically. When using a [streaming outgoing response](#streaming-outgoing-response) +without an explicit `Content-Length` response header, outgoing HTTP/1.1 response +messages will automatically use `Transfer-Encoding: chunked` while legacy HTTP/1.0 +response messages will contain the plain response body. If you know the length +of your streaming response body, you MAY want to specify it explicitly like this: + +```php +$http = new React\Http\HttpServer(function (Psr\Http\Message\ServerRequestInterface $request) { + $stream = new ThroughStream(); + + Loop::addTimer(2.0, function () use ($stream) { + $stream->end("Hello World!\n"); + }); + + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Content-Length' => '13', + 'Content-Type' => 'text/plain', + ), + $stream + ); +}); +``` + +Any response to a `HEAD` request and any response with a `1xx` (Informational), +`204` (No Content) or `304` (Not Modified) status code will *not* include a +message body as per the HTTP specs. +This means that your callback does not have to take special care of this and any +response body will simply be ignored. + +Similarly, any `2xx` (Successful) response to a `CONNECT` request, any response +with a `1xx` (Informational) or `204` (No Content) status code will *not* +include a `Content-Length` or `Transfer-Encoding` header as these do not apply +to these messages. +Note that a response to a `HEAD` request and any response with a `304` (Not +Modified) status code MAY include these headers even though +the message does not contain a response body, because these header would apply +to the message if the same request would have used an (unconditional) `GET`. + +#### Invalid response + +As stated above, each outgoing HTTP response is always represented by the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). +If your request handler function returns an invalid value or throws an +unhandled `Exception` or `Throwable`, the server will automatically send a `500` +(Internal Server Error) HTTP error response to the client. +On top of this, it will emit an `error` event that can be used for logging +purposes like this: + +```php +$http->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + if ($e->getPrevious() !== null) { + echo 'Previous: ' . $e->getPrevious()->getMessage() . PHP_EOL; + } +}); +``` + +Note that the server will also emit an `error` event if the client sends an +invalid HTTP request that never reaches your request handler function. See +also [invalid request](#invalid-request) for more details. +Additionally, a [streaming incoming request](#streaming-incoming-request) body +can also emit an `error` event on the request body. + +The server will only send a very generic `500` (Interval Server Error) HTTP +error response without any further details to the client if an unhandled +error occurs. While we understand this might make initial debugging harder, +it also means that the server does not leak any application details or stack +traces to the outside by default. It is usually recommended to catch any +`Exception` or `Throwable` within your request handler function or alternatively +use a [`middleware`](#middleware) to avoid this generic error handling and +create your own HTTP response message instead. + +#### Default response headers + +When a response is returned from the request handler function, it will be +processed by the [`HttpServer`](#httpserver) and then sent back to the client. + +A `Server: ReactPHP/1` response header will be added automatically. You can add +a custom `Server` response header like this: + +```php +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Server' => 'PHP/3' + ) + ); +}); +``` + +If you do not want to send this `Sever` response header at all (such as when you +don't want to expose the underlying server software), you can use an empty +string value like this: + +```php +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Server' => '' + ) + ); +}); +``` + +A `Date` response header will be added automatically with the current system +date and time if none is given. You can add a custom `Date` response header +like this: + +```php +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Date' => gmdate('D, d M Y H:i:s \G\M\T') + ) + ); +}); +``` + +If you do not want to send this `Date` response header at all (such as when you +don't have an appropriate clock to rely on), you can use an empty string value +like this: + +```php +$http = new React\Http\HttpServer(function (ServerRequestInterface $request) { + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Date' => '' + ) + ); +}); +``` + +The `HttpServer` class will automatically add the protocol version of the request, +so you don't have to. For instance, if the client sends the request using the +HTTP/1.1 protocol version, the response message will also use the same protocol +version, no matter what version is returned from the request handler function. + +The server supports persistent connections. An appropriate `Connection: keep-alive` +or `Connection: close` response header will be added automatically, respecting the +matching request header value and HTTP default header values. The server is +responsible for handling the `Connection` response header, so you SHOULD NOT pass +this response header yourself, unless you explicitly want to override the user's +choice with a `Connection: close` response header. + +### Middleware + +As documented above, the [`HttpServer`](#httpserver) accepts a single request handler +argument that is responsible for processing an incoming HTTP request and then +creating and returning an outgoing HTTP response. + +Many common use cases involve validating, processing, manipulating the incoming +HTTP request before passing it to the final business logic request handler. +As such, this project supports the concept of middleware request handlers. + +#### Custom middleware + +A middleware request handler is expected to adhere the following rules: + +* It is a valid `callable`. +* It accepts an instance implementing + [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) + as first argument and an optional `callable` as second argument. +* It returns either: + * An instance implementing + [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + for direct consumption. + * Any promise which can be consumed by + [`Promise\resolve()`](https://reactphp.org/promise/#resolve) resolving to a + [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + for deferred consumption. + * It MAY throw an `Exception` (or return a rejected promise) in order to + signal an error condition and abort the chain. +* It calls `$next($request)` to continue processing the next middleware + request handler or returns explicitly without calling `$next` to + abort the chain. + * The `$next` request handler (recursively) invokes the next request + handler from the chain with the same logic as above and returns (or throws) + as above. + * The `$request` may be modified prior to calling `$next($request)` to + change the incoming request the next middleware operates on. + * The `$next` return value may be consumed to modify the outgoing response. + * The `$next` request handler MAY be called more than once if you want to + implement custom "retry" logic etc. + +Note that this very simple definition allows you to use either anonymous +functions or any classes that use the magic `__invoke()` method. +This allows you to easily create custom middleware request handlers on the fly +or use a class based approach to ease using existing middleware implementations. + +While this project does provide the means to *use* middleware implementations, +it does not aim to *define* how middleware implementations should look like. +We realize that there's a vivid ecosystem of middleware implementations and +ongoing effort to standardize interfaces between these with +[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) +and support this goal. +As such, this project only bundles a few middleware implementations that are +required to match PHP's request behavior (see below) and otherwise actively +encourages [Third-Party Middleware](#third-party-middleware) implementations. + +In order to use middleware request handlers, simply pass a list of all +callables as defined above to the [`HttpServer`](#httpserver). +The following example adds a middleware request handler that adds the current time to the request as a +header (`Request-Time`) and a final request handler that always returns a `200 OK` status code without a body: + +```php +$http = new React\Http\HttpServer( + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { + $request = $request->withHeader('Request-Time', time()); + return $next($request); + }, + function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); + } +); +``` + +> Note how the middleware request handler and the final request handler have a + very simple (and similar) interface. The only difference is that the final + request handler does not receive a `$next` handler. + +Similarly, you can use the result of the `$next` middleware request handler +function to modify the outgoing response. +Note that as per the above documentation, the `$next` middleware request handler may return a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +directly or one wrapped in a promise for deferred resolution. +In order to simplify handling both paths, you can simply wrap this in a +[`Promise\resolve()`](https://reactphp.org/promise/#resolve) call like this: + +```php +$http = new React\Http\HttpServer( + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { + $promise = React\Promise\resolve($next($request)); + return $promise->then(function (ResponseInterface $response) { + return $response->withHeader('Content-Type', 'text/html'); + }); + }, + function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); + } +); +``` + +Note that the `$next` middleware request handler may also throw an +`Exception` (or return a rejected promise) as described above. +The previous example does not catch any exceptions and would thus signal an +error condition to the `HttpServer`. +Alternatively, you can also catch any `Exception` to implement custom error +handling logic (or logging etc.) by wrapping this in a +[`Promise`](https://reactphp.org/promise/#promise) like this: + +```php +$http = new React\Http\HttpServer( + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { + $promise = new React\Promise\Promise(function ($resolve) use ($next, $request) { + $resolve($next($request)); + }); + return $promise->then(null, function (Exception $e) { + return React\Http\Message\Response::plaintext( + 'Internal error: ' . $e->getMessage() . "\n" + )->withStatus(React\Http\Message\Response::STATUS_INTERNAL_SERVER_ERROR); + }); + }, + function (Psr\Http\Message\ServerRequestInterface $request) { + if (mt_rand(0, 1) === 1) { + throw new RuntimeException('Database error'); + } + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); + } +); +``` + +#### Third-Party Middleware + +While this project does provide the means to *use* middleware implementations +(see above), it does not aim to *define* how middleware implementations should +look like. We realize that there's a vivid ecosystem of middleware +implementations and ongoing effort to standardize interfaces between these with +[PSR-15](https://www.php-fig.org/psr/psr-15/) (HTTP Server Request Handlers) +and support this goal. +As such, this project only bundles a few middleware implementations that are +required to match PHP's request behavior (see +[middleware implementations](#reacthttpmiddleware)) and otherwise actively +encourages third-party middleware implementations. + +While we would love to support PSR-15 directly in `react/http`, we understand +that this interface does not specifically target async APIs and as such does +not take advantage of promises for [deferred responses](#deferred-response). +The gist of this is that where PSR-15 enforces a +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +return value, we also accept a `PromiseInterface`. +As such, we suggest using the external +[PSR-15 middleware adapter](https://github.com/friends-of-reactphp/http-middleware-psr15-adapter) +that uses on the fly monkey patching of these return values which makes using +most PSR-15 middleware possible with this package without any changes required. + +Other than that, you can also use the above [middleware definition](#middleware) +to create custom middleware. A non-exhaustive list of third-party middleware can +be found at the [middleware wiki](https://github.com/reactphp/reactphp/wiki/Users#http-middleware). +If you build or know a custom middleware, make sure to let the world know and +feel free to add it to this list. + +## API + +### Browser + +The `React\Http\Browser` is responsible for sending HTTP requests to your HTTP server +and keeps track of pending incoming HTTP responses. + +```php +$browser = new React\Http\Browser(); +``` + +This class takes two optional arguments for more advanced usage: + +```php +// constructor signature as of v1.5.0 +$browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + +// legacy constructor signature before v1.5.0 +$browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); +``` + +If you need custom connector settings (DNS resolution, TLS parameters, timeouts, +proxy servers etc.), you can explicitly pass a custom instance of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + +```php +$connector = new React\Socket\Connector(array( + 'dns' => '127.0.0.1', + 'tcp' => array( + 'bindto' => '192.168.10.1:0' + ), + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ) +)); + +$browser = new React\Http\Browser($connector); +``` + +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +> Note that the browser class is final and shouldn't be extended, it is likely to be marked final in a future release. + +#### get() + +The `get(string $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP GET request. + +```php +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also [GET request client example](examples/01-client-get-request.php). + +#### post() + +The `post(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an HTTP POST request. + +```php +$browser->post( + $url, + [ + 'Content-Type' => 'application/json' + ], + json_encode($data) +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump(json_decode((string)$response->getBody())); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also [POST JSON client example](examples/04-client-post-json.php). + +This method is also commonly used to submit HTML form data: + +```php +$data = [ + 'user' => 'Alice', + 'password' => 'secret' +]; + +$browser->post( + $url, + [ + 'Content-Type' => 'application/x-www-form-urlencoded' + ], + http_build_query($data) +); +``` + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +Loop::addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->post($url, array('Content-Length' => '11'), $body); +``` + +#### head() + +The `head(string $url, array $headers = array()): PromiseInterface` method can be used to +send an HTTP HEAD request. + +```php +$browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +#### patch() + +The `patch(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an HTTP PATCH request. + +```php +$browser->patch( + $url, + [ + 'Content-Type' => 'application/json' + ], + json_encode($data) +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump(json_decode((string)$response->getBody())); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +Loop::addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->patch($url, array('Content-Length' => '11'), $body); +``` + +#### put() + +The `put(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an HTTP PUT request. + +```php +$browser->put( + $url, + [ + 'Content-Type' => 'text/xml' + ], + $xml->asXML() +)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also [PUT XML client example](examples/05-client-put-xml.php). + +This method will automatically add a matching `Content-Length` request +header if the outgoing request body is a `string`. If you're using a +streaming request body (`ReadableStreamInterface`), it will default to +using `Transfer-Encoding: chunked` or you have to explicitly pass in a +matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +Loop::addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->put($url, array('Content-Length' => '11'), $body); +``` + +#### delete() + +The `delete(string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an HTTP DELETE request. + +```php +$browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +#### request() + +The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an arbitrary HTTP request. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. + +As an alternative, if you want to use a custom HTTP request method, you +can use this method: + +```php +$browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump((string)$response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +This method will automatically add a matching `Content-Length` request +header if the size of the outgoing request body is known and non-empty. +For an empty request body, if will only include a `Content-Length: 0` +request header if the request method usually expects a request body (only +applies to `POST`, `PUT` and `PATCH`). + +If you're using a streaming request body (`ReadableStreamInterface`), it +will default to using `Transfer-Encoding: chunked` or you have to +explicitly pass in a matching `Content-Length` request header like so: + +```php +$body = new React\Stream\ThroughStream(); +Loop::addTimer(1.0, function () use ($body) { + $body->end("hello world"); +}); + +$browser->request('POST', $url, array('Content-Length' => '11'), $body); +``` + +#### requestStreaming() + +The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to +send an arbitrary HTTP request and receive a streaming response without buffering the response body. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. Each of these methods will buffer +the whole response body in memory by default. This is easy to get started +and works reasonably well for smaller responses. + +In some situations, it's a better idea to use a streaming approach, where +only small chunks have to be kept in memory. You can use this method to +send an arbitrary HTTP request and receive a streaming response. It uses +the same HTTP message API, but does not buffer the response body in +memory. It only processes the response body in small chunks as data is +received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). +This works for (any number of) responses of arbitrary sizes. + +```php +$browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + $body = $response->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + $body->on('data', function ($chunk) { + echo $chunk; + }); + + $body->on('error', function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + }); + + $body->on('close', function () { + echo '[DONE]' . PHP_EOL; + }); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also [ReactPHP's `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); +``` + +#### withTimeout() + +The `withTimeout(bool|number $timeout): Browser` method can be used to +change 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. + +#### withFollowRedirects() + +The `withFollowRedirects(bool|int $followRedirects): Browser` method can be used to +change how HTTP redirects will be followed. + +You can pass in the maximum number of redirects to follow: + +```php +$browser = $browser->withFollowRedirects(5); +``` + +The request will automatically be rejected when the number of redirects +is exceeded. You can pass in a `0` to reject the request for any +redirects encountered: + +```php +$browser = $browser->withFollowRedirects(0); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // only non-redirected responses will now end up here + var_dump($response->getHeaders()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +You can pass in a bool `false` to disable following any redirects. In +this case, requests will resolve with the redirection response instead +of following the `Location` response header: + +```php +$browser = $browser->withFollowRedirects(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any redirects will now end up here + var_dump($response->getHeaderLine('Location')); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +You can pass in a bool `true` to re-enable default redirect handling. +This defaults to following a maximum of 10 redirects: + +```php +$browser = $browser->withFollowRedirects(true); +``` + +See also [redirects](#redirects) for more details about redirect handling. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given redirect setting applied. + +#### withRejectErrorResponse() + +The `withRejectErrorResponse(bool $obeySuccessCode): Browser` method can be used to +change whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + +You can pass in a bool `false` to disable rejecting incoming responses +that use a 4xx or 5xx response status code. In this case, requests will +resolve with the response message indicating an error condition: + +```php +$browser = $browser->withRejectErrorResponse(false); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any HTTP response will now end up here + var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +You can pass in a bool `true` to re-enable default status code handling. +This defaults to rejecting any response status codes in the 4xx or 5xx +range with a [`ResponseException`](#responseexception): + +```php +$browser = $browser->withRejectErrorResponse(true); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // any successful HTTP response will now end up here + var_dump($response->getStatusCode(), $response->getReasonPhrase()); +}, function (Exception $e) { + if ($e instanceof React\Http\Message\ResponseException) { + // any HTTP response error message will now end up here + $response = $e->getResponse(); + var_dump($response->getStatusCode(), $response->getReasonPhrase()); + } else { + echo 'Error: ' . $e->getMessage() . PHP_EOL; + } +}); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given setting applied. + +#### withBase() + +The `withBase(string|null $baseUrl): Browser` method can be used to +change the base URL used to resolve relative URLs to. + +If you configure a base URL, any requests to relative URLs will be +processed by first resolving this relative to the given absolute base +URL. This supports resolving relative path references (like `../` etc.). +This is particularly useful for (RESTful) API calls where all endpoints +(URLs) are located under a common base URL. + +```php +$browser = $browser->withBase('http://api.example.com/v3/'); + +// will request http://api.example.com/v3/users +$browser->get('users')->then(…); +``` + +You can pass in a `null` base URL to return a new instance that does not +use a base URL: + +```php +$browser = $browser->withBase(null); +``` + +Accordingly, any requests using relative URLs to a browser that does not +use a base URL can not be completed and will be rejected without sending +a request. + +This method will throw an `InvalidArgumentException` if the given +`$baseUrl` argument is not a valid URL. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method +actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + +#### withProtocolVersion() + +The `withProtocolVersion(string $protocolVersion): Browser` method can be used to +change the HTTP protocol version that will be used for all subsequent requests. + +All the above [request methods](#request-methods) default to sending +requests as HTTP/1.1. This is the preferred HTTP protocol version which +also provides decent backwards-compatibility with legacy HTTP/1.0 +servers. As such, there should rarely be a need to explicitly change this +protocol version. + +If you want to explicitly use the legacy HTTP/1.0 protocol version, you +can use this method: + +```php +$browser = $browser->withProtocolVersion('1.0'); + +$browser->get($url)->then(…); +``` + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +new protocol version applied. + +#### withResponseBuffer() + +The `withResponseBuffer(int $maximumSize): Browser` method can be used to +change the maximum size for buffering a response body. + +The preferred way to send an HTTP request is by using the above +[request methods](#request-methods), for example the [`get()`](#get) +method to send an HTTP `GET` request. Each of these methods will buffer +the whole response body in memory by default. This is easy to get started +and works reasonably well for smaller responses. + +By default, the response body buffer will be limited to 16 MiB. If the +response body exceeds this maximum size, the request will be rejected. + +You can pass in the maximum number of bytes to buffer: + +```php +$browser = $browser->withResponseBuffer(1024 * 1024); + +$browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + // response body will not exceed 1 MiB + var_dump($response->getHeaders(), (string) $response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that the response body buffer has to be kept in memory for each +pending request until its transfer is completed and it will only be freed +after a pending request is fulfilled. As such, increasing this maximum +buffer size to allow larger response bodies is usually not recommended. +Instead, you can use the [`requestStreaming()` method](#requeststreaming) +to receive responses with arbitrary sizes without buffering. Accordingly, +this maximum buffer size setting has no effect on streaming responses. + +Notice that the [`Browser`](#browser) is an immutable object, i.e. this +method actually returns a *new* [`Browser`](#browser) instance with the +given setting applied. + +#### withHeader() + +The `withHeader(string $header, string $value): Browser` method can be used to +add a request header for all following requests. + +```php +$browser = $browser->withHeader('User-Agent', 'ACME'); + +$browser->get($url)->then(…); +``` + +Note that the new header will overwrite any headers previously set with +the same name (case-insensitive). Following requests will use these headers +by default unless they are explicitly set for any requests. + +#### withoutHeader() + +The `withoutHeader(string $header): Browser` method can be used to +remove any default request headers previously set via +the [`withHeader()` method](#withheader). + +```php +$browser = $browser->withoutHeader('User-Agent'); + +$browser->get($url)->then(…); +``` + +Note that this method only affects the headers which were set with the +method `withHeader(string $header, string $value): Browser` + +### React\Http\Message + +#### Response + +The `React\Http\Message\Response` class can be used to +represent an outgoing server response message. + +```php +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Content-Type' => 'text/html' + ), + "Hello world!\n" +); +``` + +This class implements the +[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) +which in turn extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +On top of this, this class implements the +[PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) +which means that most common HTTP status codes are available as class +constants with the `STATUS_*` prefix. For instance, the `200 OK` and +`404 Not Found` status codes can used as `Response::STATUS_OK` and +`Response::STATUS_NOT_FOUND` respectively. + +> Internally, this implementation builds on top of a base class which is + considered an implementation detail that may change in the future. + +##### html() + +The static `html(string $html): Response` method can be used to +create an HTML response. + +```php +$html = << + +Hello wörld! + + +HTML; + +$response = React\Http\Message\Response::html($html); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'text/html; charset=utf-8' + ], + $html +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given HTTP source +string encoded in UTF-8 (Unicode). It's generally recommended to end the +given plaintext string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::html( + "

Error

\n

Invalid user name given.

\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### json() + +The static `json(mixed $data): Response` method can be used to +create a JSON response. + +```php +$response = React\Http\Message\Response::json(['name' => 'Alice']); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'application/json' + ], + json_encode( + ['name' => 'Alice'], + JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + ) . "\n" +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given structured +data encoded as a JSON text. + +The given structured data will be encoded as a JSON text. Any `string` +values in the data must be encoded in UTF-8 (Unicode). If the encoding +fails, this method will throw an `InvalidArgumentException`. + +By default, the given structured data will be encoded with the flags as +shown above. This includes pretty printing (PHP 5.4+) and preserving +zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is +assumed any additional data overhead is usually compensated by using HTTP +response compression. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::json( + ['error' => 'Invalid user name given'] +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### plaintext() + +The static `plaintext(string $text): Response` method can be used to +create a plaintext response. + +```php +$response = React\Http\Message\Response::plaintext("Hello wörld!\n"); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'text/plain; charset=utf-8' + ], + "Hello wörld!\n" +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given plaintext +string encoded in UTF-8 (Unicode). It's generally recommended to end the +given plaintext string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::plaintext( + "Error: Invalid user name given.\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +##### xml() + +The static `xml(string $xml): Response` method can be used to +create an XML response. + +```php +$xml = << + + Hello wörld! + + +XML; + +$response = React\Http\Message\Response::xml($xml); +``` + +This is a convenient shortcut method that returns the equivalent of this: + +``` +$response = new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [ + 'Content-Type' => 'application/xml' + ], + $xml +); +``` + +This method always returns a response with a `200 OK` status code and +the appropriate `Content-Type` response header for the given XML source +string. It's generally recommended to use UTF-8 (Unicode) and specify +this as part of the leading XML declaration and to end the given XML +source string with a trailing newline. + +If you want to use a different status code or custom HTTP response +headers, you can manipulate the returned response object using the +provided PSR-7 methods or directly instantiate a custom HTTP response +object using the `Response` constructor: + +```php +$response = React\Http\Message\Response::xml( + "Invalid user name given.\n" +)->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); +``` + +#### Request + +The `React\Http\Message\Request` class can be used to +respresent an outgoing HTTP request message. + +This class implements the +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) +which extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +This is mostly used internally to represent each outgoing HTTP request +message for the HTTP client implementation. Likewise, you can also use this +class with other HTTP client implementations and for tests. + +> Internally, this implementation builds on top of a base class which is + considered an implementation detail that may change in the future. + +#### ServerRequest + +The `React\Http\Message\ServerRequest` class can be used to +respresent an incoming server request message. + +This class implements the +[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) +which extends the +[PSR-7 `RequestInterface`](https://www.php-fig.org/psr/psr-7/#32-psrhttpmessagerequestinterface) +which in turn extends the +[PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + +This is mostly used internally to represent each incoming request message. +Likewise, you can also use this class in test cases to test how your web +application reacts to certain HTTP requests. + +> Internally, this implementation builds on top of a base class which is + considered an implementation detail that may change in the future. + +#### Uri + +The `React\Http\Message\Uri` class can be used to +respresent a URI (or URL). + +This class implements the +[PSR-7 `UriInterface`](https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface). + +This is mostly used internally to represent the URI of each HTTP request +message for our HTTP client and server implementations. Likewise, you may +also use this class with other HTTP implementations and for tests. + +#### ResponseException + +The `React\Http\Message\ResponseException` is an `Exception` sub-class that will be used to reject +a request promise if the remote server returns a non-success status code +(anything but 2xx or 3xx). +You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). + +The `getCode(): int` method can be used to +return the HTTP response status code. + +The `getResponse(): ResponseInterface` method can be used to +access its underlying response object. + +### React\Http\Middleware + +#### StreamingRequestMiddleware + +The `React\Http\Middleware\StreamingRequestMiddleware` can be used to +process incoming requests with a streaming request body (without buffering). + +This allows you to process requests of any size without buffering the request +body in memory. Instead, it will represent the request body as a +[`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) +that emit chunks of incoming data as it is received: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + function (Psr\Http\Message\ServerRequestInterface $request) { + $body = $request->getBody(); + assert($body instanceof Psr\Http\Message\StreamInterface); + assert($body instanceof React\Stream\ReadableStreamInterface); + + return new React\Promise\Promise(function ($resolve) use ($body) { + $bytes = 0; + $body->on('data', function ($chunk) use (&$bytes) { + $bytes += \count($chunk); + }); + $body->on('close', function () use (&$bytes, $resolve) { + $resolve(new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + [], + "Received $bytes bytes\n" + )); + }); + }); + } +); +``` + +See also [streaming incoming request](#streaming-incoming-request) +for more details. + +Additionally, this middleware can be used in combination with the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to explicitly configure the total number of requests that can be handled at +once: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +); +``` + +> Internally, this class is used as a "marker" to not trigger the default + request buffering behavior in the `HttpServer`. It does not implement any logic + on its own. + +#### LimitConcurrentRequestsMiddleware + +The `React\Http\Middleware\LimitConcurrentRequestsMiddleware` can be used to +limit how many next handlers can be executed concurrently. + +If this middleware is invoked, it will check if the number of pending +handlers is below the allowed limit and then simply invoke the next handler +and it will return whatever the next handler returns (or throws). + +If the number of pending handlers exceeds the allowed limit, the request will +be queued (and its streaming body will be paused) and it will return a pending +promise. +Once a pending handler returns (or throws), it will pick the oldest request +from this queue and invokes the next handler (and its streaming body will be +resumed). + +The following example shows how this middleware can be used to ensure no more +than 10 handlers will be invoked at once: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(10), + $handler +); +``` + +Similarly, this middleware is often used in combination with the +[`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) +to limit the total number of requests that can be buffered at once: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +); +``` + +More sophisticated examples include limiting the total number of requests +that can be buffered at once and then ensure the actual request handler only +processes one request after another without any concurrency: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + new React\Http\Middleware\RequestBodyParserMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(1), // only execute 1 handler (no concurrency) + $handler +); +``` + +#### RequestBodyBufferMiddleware + +One of the built-in middleware is the `React\Http\Middleware\RequestBodyBufferMiddleware` which +can be used to buffer the whole incoming request body in memory. +This can be useful if full PSR-7 compatibility is needed for the request handler +and the default streaming request body handling is not needed. +The constructor accepts one optional argument, the maximum request body size. +When one isn't provided it will use `post_max_size` (default 8 MiB) from PHP's +configuration. +(Note that the value from your matching SAPI will be used, which is the CLI +configuration in most cases.) + +Any incoming request that has a request body that exceeds this limit will be +accepted, but its request body will be discarded (empty request body). +This is done in order to avoid having to keep an incoming request with an +excessive size (for example, think of a 2 GB file upload) in memory. +This allows the next middleware handler to still handle this request, but it +will see an empty request body. +This is similar to PHP's default behavior, where the body will not be parsed +if this limit is exceeded. However, unlike PHP's default behavior, the raw +request body is not available via `php://input`. + +The `RequestBodyBufferMiddleware` will buffer requests with bodies of known size +(i.e. with `Content-Length` header specified) as well as requests with bodies of +unknown size (i.e. with `Transfer-Encoding: chunked` header). + +All requests will be buffered in memory until the request body end has +been reached and then call the next middleware handler with the complete, +buffered request. +Similarly, this will immediately invoke the next middleware handler for requests +that have an empty request body (such as a simple `GET` request) and requests +that are already buffered (such as due to another middleware). + +Note that the given buffer size limit is applied to each request individually. +This means that if you allow a 2 MiB limit and then receive 1000 concurrent +requests, up to 2000 MiB may be allocated for these buffers alone. +As such, it's highly recommended to use this along with the +[`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) (see above) to limit +the total number of concurrent requests. + +Usage: + +```php +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + function (Psr\Http\Message\ServerRequestInterface $request) { + // The body from $request->getBody() is now fully available without the need to stream it + return new React\Http\Message\Response(React\Http\Message\Response::STATUS_OK); + }, +); +``` + +#### RequestBodyParserMiddleware + +The `React\Http\Middleware\RequestBodyParserMiddleware` takes a fully buffered request body +(generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)), +and parses the form values and file uploads from the incoming HTTP request body. + +This middleware handler takes care of applying values from HTTP +requests that use `Content-Type: application/x-www-form-urlencoded` or +`Content-Type: multipart/form-data` to resemble PHP's default superglobals +`$_POST` and `$_FILES`. +Instead of relying on these superglobals, you can use the +`$request->getParsedBody()` and `$request->getUploadedFiles()` methods +as defined by PSR-7. + +Accordingly, each file upload will be represented as instance implementing the +[PSR-7 `UploadedFileInterface`](https://www.php-fig.org/psr/psr-7/#36-psrhttpmessageuploadedfileinterface). +Due to its blocking nature, the `moveTo()` method is not available and throws +a `RuntimeException` instead. +You can use `$contents = (string)$file->getStream();` to access the file +contents and persist this to your favorite data store. + +```php +$handler = function (Psr\Http\Message\ServerRequestInterface $request) { + // If any, parsed form fields are now available from $request->getParsedBody() + $body = $request->getParsedBody(); + $name = isset($body['name']) ? $body['name'] : 'unnamed'; + + $files = $request->getUploadedFiles(); + $avatar = isset($files['avatar']) ? $files['avatar'] : null; + if ($avatar instanceof Psr\Http\Message\UploadedFileInterface) { + if ($avatar->getError() === UPLOAD_ERR_OK) { + $uploaded = $avatar->getSize() . ' bytes'; + } elseif ($avatar->getError() === UPLOAD_ERR_INI_SIZE) { + $uploaded = 'file too large'; + } else { + $uploaded = 'with error'; + } + } else { + $uploaded = 'nothing'; + } + + return new React\Http\Message\Response( + React\Http\Message\Response::STATUS_OK, + array( + 'Content-Type' => 'text/plain' + ), + $name . ' uploaded ' . $uploaded + ); +}; + +$http = new React\Http\HttpServer( + new React\Http\Middleware\StreamingRequestMiddleware(), + new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + new React\Http\Middleware\RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB + new React\Http\Middleware\RequestBodyParserMiddleware(), + $handler +); +``` + +See also [form upload server example](examples/62-server-form-upload.php) for more details. + +By default, this middleware respects the +[`upload_max_filesize`](https://www.php.net/manual/en/ini.core.php#ini.upload-max-filesize) +(default `2M`) ini setting. +Files that exceed this limit will be rejected with an `UPLOAD_ERR_INI_SIZE` error. +You can control the maximum filesize for each individual file upload by +explicitly passing the maximum filesize in bytes as the first parameter to the +constructor like this: + +```php +new React\Http\Middleware\RequestBodyParserMiddleware(8 * 1024 * 1024); // 8 MiB limit per file +``` + +By default, this middleware respects the +[`file_uploads`](https://www.php.net/manual/en/ini.core.php#ini.file-uploads) +(default `1`) and +[`max_file_uploads`](https://www.php.net/manual/en/ini.core.php#ini.max-file-uploads) +(default `20`) ini settings. +These settings control if any and how many files can be uploaded in a single request. +If you upload more files in a single request, additional files will be ignored +and the `getUploadedFiles()` method returns a truncated array. +Note that upload fields left blank on submission do not count towards this limit. +You can control the maximum number of file uploads per request by explicitly +passing the second parameter to the constructor like this: + +```php +new React\Http\Middleware\RequestBodyParserMiddleware(10 * 1024, 100); // 100 files with 10 KiB each +``` + +> Note that this middleware handler simply parses everything that is already + buffered in the request body. + It is imperative that the request body is buffered by a prior middleware + handler as given in the example above. + This previous middleware handler is also responsible for rejecting incoming + requests that exceed allowed message sizes (such as big file uploads). + The [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) used above + simply discards excessive request bodies, resulting in an empty body. + If you use this middleware without buffering first, it will try to parse an + empty (streaming) body and may thus assume an empty data structure. + See also [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) for + more details. + +> PHP's `MAX_FILE_SIZE` hidden field is respected by this middleware. + Files that exceed this limit will be rejected with an `UPLOAD_ERR_FORM_SIZE` error. + +> This middleware respects the + [`max_input_vars`](https://www.php.net/manual/en/info.configuration.php#ini.max-input-vars) + (default `1000`) and + [`max_input_nesting_level`](https://www.php.net/manual/en/info.configuration.php#ini.max-input-nesting-level) + (default `64`) ini settings. + +> Note that this middleware ignores the + [`enable_post_data_reading`](https://www.php.net/manual/en/ini.core.php#ini.enable-post-data-reading) + (default `1`) ini setting because it makes little sense to respect here and + is left up to higher-level implementations. + If you want to respect this setting, you have to check its value and + effectively avoid using this middleware entirely. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org/). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](https://semver.org/). +This will install the latest supported version: + +```bash +composer require react/http:^1.11 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and +HHVM. +It's *highly recommended to use the latest supported PHP version* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +vendor/bin/phpunit --exclude-group internet +``` + +## License + +MIT, see [LICENSE file](LICENSE). diff --git a/vendor/react/http/composer.json b/vendor/react/http/composer.json new file mode 100644 index 0000000..4234210 --- /dev/null +++ b/vendor/react/http/composer.json @@ -0,0 +1,57 @@ +{ + "name": "react/http", + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": ["HTTP client", "HTTP server", "HTTP", "HTTPS", "event-driven", "streaming", "client", "server", "PSR-7", "async", "ReactPHP"], + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.3 || ^1.2.1", + "react/socket": "^1.16", + "react/stream": "^1.4" + }, + "require-dev": { + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.2 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "autoload": { + "psr-4": { + "React\\Http\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Http\\": "tests/" + } + } +} diff --git a/vendor/react/http/src/Browser.php b/vendor/react/http/src/Browser.php new file mode 100644 index 0000000..9da0dca --- /dev/null +++ b/vendor/react/http/src/Browser.php @@ -0,0 +1,858 @@ + 'ReactPHP/1' + ); + + /** + * The `Browser` is responsible for sending HTTP requests to your HTTP server + * and keeps track of pending incoming HTTP responses. + * + * ```php + * $browser = new React\Http\Browser(); + * ``` + * + * This class takes two optional arguments for more advanced usage: + * + * ```php + * // constructor signature as of v1.5.0 + * $browser = new React\Http\Browser(?ConnectorInterface $connector = null, ?LoopInterface $loop = null); + * + * // legacy constructor signature before v1.5.0 + * $browser = new React\Http\Browser(?LoopInterface $loop = null, ?ConnectorInterface $connector = null); + * ``` + * + * If you need custom connector settings (DNS resolution, TLS parameters, timeouts, + * proxy servers etc.), you can explicitly pass a custom instance of the + * [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + * + * ```php + * $connector = new React\Socket\Connector(array( + * 'dns' => '127.0.0.1', + * 'tcp' => array( + * 'bindto' => '192.168.10.1:0' + * ), + * 'tls' => array( + * 'verify_peer' => false, + * 'verify_peer_name' => false + * ) + * )); + * + * $browser = new React\Http\Browser($connector); + * ``` + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param null|ConnectorInterface|LoopInterface $connector + * @param null|LoopInterface|ConnectorInterface $loop + * @throws \InvalidArgumentException for invalid arguments + */ + public function __construct($connector = null, $loop = null) + { + // swap arguments for legacy constructor signature + if (($connector instanceof LoopInterface || $connector === null) && ($loop instanceof ConnectorInterface || $loop === null)) { + $swap = $loop; + $loop = $connector; + $connector = $swap; + } + + if (($connector !== null && !$connector instanceof ConnectorInterface) || ($loop !== null && !$loop instanceof LoopInterface)) { + throw new \InvalidArgumentException('Expected "?ConnectorInterface $connector" and "?LoopInterface $loop" arguments'); + } + + $loop = $loop ?: Loop::get(); + $this->transaction = new Transaction( + Sender::createFromLoop($loop, $connector ?: new Connector(array(), $loop)), + $loop + ); + } + + /** + * Sends an HTTP GET request + * + * ```php + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [GET request client example](../examples/01-client-get-request.php). + * + * @param string $url URL for the request. + * @param array $headers + * @return PromiseInterface + */ + public function get($url, array $headers = array()) + { + return $this->requestMayBeStreaming('GET', $url, $headers); + } + + /** + * Sends an HTTP POST request + * + * ```php + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [POST JSON client example](../examples/04-client-post-json.php). + * + * This method is also commonly used to submit HTML form data: + * + * ```php + * $data = [ + * 'user' => 'Alice', + * 'password' => 'secret' + * ]; + * + * $browser->post( + * $url, + * [ + * 'Content-Type' => 'application/x-www-form-urlencoded' + * ], + * http_build_query($data) + * ); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->post($url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface + */ + public function post($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('POST', $url, $headers, $body); + } + + /** + * Sends an HTTP HEAD request + * + * ```php + * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @return PromiseInterface + */ + public function head($url, array $headers = array()) + { + return $this->requestMayBeStreaming('HEAD', $url, $headers); + } + + /** + * Sends an HTTP PATCH request + * + * ```php + * $browser->patch( + * $url, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode($data) + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump(json_decode((string)$response->getBody())); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->patch($url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface + */ + public function patch($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('PATCH', $url , $headers, $body); + } + + /** + * Sends an HTTP PUT request + * + * ```php + * $browser->put( + * $url, + * [ + * 'Content-Type' => 'text/xml' + * ], + * $xml->asXML() + * )->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [PUT XML client example](../examples/05-client-put-xml.php). + * + * This method will automatically add a matching `Content-Length` request + * header if the outgoing request body is a `string`. If you're using a + * streaming request body (`ReadableStreamInterface`), it will default to + * using `Transfer-Encoding: chunked` or you have to explicitly pass in a + * matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->put($url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface + */ + public function put($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('PUT', $url, $headers, $body); + } + + /** + * Sends an HTTP DELETE request + * + * ```php + * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * @param string $url URL for the request. + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface + */ + public function delete($url, array $headers = array(), $body = '') + { + return $this->requestMayBeStreaming('DELETE', $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. + * + * As an alternative, if you want to use a custom HTTP request method, you + * can use this method: + * + * ```php + * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * var_dump((string)$response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->request('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface + */ + public function request($method, $url, array $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * In some situations, it's a better idea to use a streaming approach, where + * only small chunks have to be kept in memory. You can use this method to + * send an arbitrary HTTP request and receive a streaming response. It uses + * the same HTTP message API, but does not buffer the response body in + * memory. It only processes the response body in small chunks as data is + * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). + * This works for (any number of) responses of arbitrary sizes. + * + * ```php + * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * $body = $response->getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * $body->on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $body->on('error', function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * + * $body->on('close', function () { + * echo '[DONE]' . PHP_EOL; + * }); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) + * and the [streaming response](#streaming-response) for more details, + * examples and possible use-cases. + * + * This method will automatically add a matching `Content-Length` request + * header if the size of the outgoing request body is known and non-empty. + * For an empty request body, if will only include a `Content-Length: 0` + * request header if the request method usually expects a request body (only + * applies to `POST`, `PUT` and `PATCH`). + * + * If you're using a streaming request body (`ReadableStreamInterface`), it + * will default to using `Transfer-Encoding: chunked` or you have to + * explicitly pass in a matching `Content-Length` request header like so: + * + * ```php + * $body = new React\Stream\ThroughStream(); + * Loop::addTimer(1.0, function () use ($body) { + * $body->end("hello world"); + * }); + * + * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); + * ``` + * + * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. + * @param string $url URL for the request + * @param array $headers Additional request headers + * @param string|ReadableStreamInterface $body HTTP request body contents + * @return PromiseInterface + */ + public function requestStreaming($method, $url, $headers = array(), $body = '') + { + return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $body); + } + + /** + * Changes the maximum timeout used for waiting for pending requests. + * + * You can pass in the number of seconds to use as a new timeout value: + * + * ```php + * $browser = $browser->withTimeout(10.0); + * ``` + * + * You can pass in a bool `false` to disable any timeouts. In this case, + * requests can stay pending forever: + * + * ```php + * $browser = $browser->withTimeout(false); + * ``` + * + * You can pass in a bool `true` to re-enable default timeout handling. This + * will respects PHP's `default_socket_timeout` setting (default 60s): + * + * ```php + * $browser = $browser->withTimeout(true); + * ``` + * + * See also [timeouts](#timeouts) for more details about timeout handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given timeout value applied. + * + * @param bool|number $timeout + * @return self + */ + public function withTimeout($timeout) + { + if ($timeout === true) { + $timeout = null; + } elseif ($timeout === false) { + $timeout = -1; + } elseif ($timeout < 0) { + $timeout = 0; + } + + return $this->withOptions(array( + 'timeout' => $timeout, + )); + } + + /** + * Changes how HTTP redirects will be followed. + * + * You can pass in the maximum number of redirects to follow: + * + * ```php + * $browser = $browser->withFollowRedirects(5); + * ``` + * + * The request will automatically be rejected when the number of redirects + * is exceeded. You can pass in a `0` to reject the request for any + * redirects encountered: + * + * ```php + * $browser = $browser->withFollowRedirects(0); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // only non-redirected responses will now end up here + * var_dump($response->getHeaders()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * You can pass in a bool `false` to disable following any redirects. In + * this case, requests will resolve with the redirection response instead + * of following the `Location` response header: + * + * ```php + * $browser = $browser->withFollowRedirects(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any redirects will now end up here + * var_dump($response->getHeaderLine('Location')); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default redirect handling. + * This defaults to following a maximum of 10 redirects: + * + * ```php + * $browser = $browser->withFollowRedirects(true); + * ``` + * + * See also [redirects](#redirects) for more details about redirect handling. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given redirect setting applied. + * + * @param bool|int $followRedirects + * @return self + */ + public function withFollowRedirects($followRedirects) + { + return $this->withOptions(array( + 'followRedirects' => $followRedirects !== false, + 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects + )); + } + + /** + * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. + * + * You can pass in a bool `false` to disable rejecting incoming responses + * that use a 4xx or 5xx response status code. In this case, requests will + * resolve with the response message indicating an error condition: + * + * ```php + * $browser = $browser->withRejectErrorResponse(false); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * You can pass in a bool `true` to re-enable default status code handling. + * This defaults to rejecting any response status codes in the 4xx or 5xx + * range: + * + * ```php + * $browser = $browser->withRejectErrorResponse(true); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // any successful HTTP response will now end up here + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * }, function (Exception $e) { + * if ($e instanceof React\Http\Message\ResponseException) { + * // any HTTP response error message will now end up here + * $response = $e->getResponse(); + * var_dump($response->getStatusCode(), $response->getReasonPhrase()); + * } else { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * } + * }); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param bool $obeySuccessCode + * @return self + */ + public function withRejectErrorResponse($obeySuccessCode) + { + return $this->withOptions(array( + 'obeySuccessCode' => $obeySuccessCode, + )); + } + + /** + * Changes the base URL used to resolve relative URLs to. + * + * If you configure a base URL, any requests to relative URLs will be + * processed by first resolving this relative to the given absolute base + * URL. This supports resolving relative path references (like `../` etc.). + * This is particularly useful for (RESTful) API calls where all endpoints + * (URLs) are located under a common base URL. + * + * ```php + * $browser = $browser->withBase('http://api.example.com/v3/'); + * + * // will request http://api.example.com/v3/users + * $browser->get('users')->then(…); + * ``` + * + * You can pass in a `null` base URL to return a new instance that does not + * use a base URL: + * + * ```php + * $browser = $browser->withBase(null); + * ``` + * + * Accordingly, any requests using relative URLs to a browser that does not + * use a base URL can not be completed and will be rejected without sending + * a request. + * + * This method will throw an `InvalidArgumentException` if the given + * `$baseUrl` argument is not a valid URL. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method + * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. + * + * @param string|null $baseUrl absolute base URL + * @return self + * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL + * @see self::withoutBase() + */ + public function withBase($baseUrl) + { + $browser = clone $this; + if ($baseUrl === null) { + $browser->baseUrl = null; + return $browser; + } + + $browser->baseUrl = new Uri($baseUrl); + if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { + throw new \InvalidArgumentException('Base URL must be absolute'); + } + + return $browser; + } + + /** + * Changes the HTTP protocol version that will be used for all subsequent requests. + * + * All the above [request methods](#request-methods) default to sending + * requests as HTTP/1.1. This is the preferred HTTP protocol version which + * also provides decent backwards-compatibility with legacy HTTP/1.0 + * servers. As such, there should rarely be a need to explicitly change this + * protocol version. + * + * If you want to explicitly use the legacy HTTP/1.0 protocol version, you + * can use this method: + * + * ```php + * $browser = $browser->withProtocolVersion('1.0'); + * + * $browser->get($url)->then(…); + * ``` + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * new protocol version applied. + * + * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" + * @return self + * @throws InvalidArgumentException + */ + public function withProtocolVersion($protocolVersion) + { + if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { + throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); + } + + $browser = clone $this; + $browser->protocolVersion = (string) $protocolVersion; + + return $browser; + } + + /** + * Changes the maximum size for buffering a response body. + * + * The preferred way to send an HTTP request is by using the above + * [request methods](#request-methods), for example the [`get()`](#get) + * method to send an HTTP `GET` request. Each of these methods will buffer + * the whole response body in memory by default. This is easy to get started + * and works reasonably well for smaller responses. + * + * By default, the response body buffer will be limited to 16 MiB. If the + * response body exceeds this maximum size, the request will be rejected. + * + * You can pass in the maximum number of bytes to buffer: + * + * ```php + * $browser = $browser->withResponseBuffer(1024 * 1024); + * + * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { + * // response body will not exceed 1 MiB + * var_dump($response->getHeaders(), (string) $response->getBody()); + * }, function (Exception $e) { + * echo 'Error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * Note that the response body buffer has to be kept in memory for each + * pending request until its transfer is completed and it will only be freed + * after a pending request is fulfilled. As such, increasing this maximum + * buffer size to allow larger response bodies is usually not recommended. + * Instead, you can use the [`requestStreaming()` method](#requeststreaming) + * to receive responses with arbitrary sizes without buffering. Accordingly, + * this maximum buffer size setting has no effect on streaming responses. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * given setting applied. + * + * @param int $maximumSize + * @return self + * @see self::requestStreaming() + */ + public function withResponseBuffer($maximumSize) + { + return $this->withOptions(array( + 'maximumSize' => $maximumSize + )); + } + + /** + * Add a request header for all following requests. + * + * ```php + * $browser = $browser->withHeader('User-Agent', 'ACME'); + * + * $browser->get($url)->then(…); + * ``` + * + * Note that the new header will overwrite any headers previously set with + * the same name (case-insensitive). Following requests will use these headers + * by default unless they are explicitly set for any requests. + * + * @param string $header + * @param string $value + * @return Browser + */ + public function withHeader($header, $value) + { + $browser = $this->withoutHeader($header); + $browser->defaultHeaders[$header] = $value; + + return $browser; + } + + /** + * Remove any default request headers previously set via + * the [`withHeader()` method](#withheader). + * + * ```php + * $browser = $browser->withoutHeader('User-Agent'); + * + * $browser->get($url)->then(…); + * ``` + * + * Note that this method only affects the headers which were set with the + * method `withHeader(string $header, string $value): Browser` + * + * @param string $header + * @return Browser + */ + public function withoutHeader($header) + { + $browser = clone $this; + + /** @var string|int $key */ + foreach (\array_keys($browser->defaultHeaders) as $key) { + if (\strcasecmp($key, $header) === 0) { + unset($browser->defaultHeaders[$key]); + break; + } + } + + return $browser; + } + + /** + * Changes the [options](#options) to use: + * + * The [`Browser`](#browser) class exposes several options for the handling of + * HTTP transactions. These options resemble some of PHP's + * [HTTP context options](http://php.net/manual/en/context.http.php) and + * can be controlled via the following API (and their defaults): + * + * ```php + * // deprecated + * $newBrowser = $browser->withOptions(array( + * 'timeout' => null, // see withTimeout() instead + * 'followRedirects' => true, // see withFollowRedirects() instead + * 'maxRedirects' => 10, // see withFollowRedirects() instead + * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead + * 'streaming' => false, // deprecated, see requestStreaming() instead + * )); + * ``` + * + * See also [timeouts](#timeouts), [redirects](#redirects) and + * [streaming](#streaming) for more details. + * + * Notice that the [`Browser`](#browser) is an immutable object, i.e. this + * method actually returns a *new* [`Browser`](#browser) instance with the + * options applied. + * + * @param array $options + * @return self + * @see self::withTimeout() + * @see self::withFollowRedirects() + * @see self::withRejectErrorResponse() + */ + private function withOptions(array $options) + { + $browser = clone $this; + $browser->transaction = $this->transaction->withOptions($options); + + return $browser; + } + + /** + * @param string $method + * @param string $url + * @param array $headers + * @param string|ReadableStreamInterface $body + * @return PromiseInterface + */ + private function requestMayBeStreaming($method, $url, array $headers = array(), $body = '') + { + if ($this->baseUrl !== null) { + // ensure we're actually below the base URL + $url = Uri::resolve($this->baseUrl, new Uri($url)); + } + + foreach ($this->defaultHeaders as $key => $value) { + $explicitHeaderExists = false; + foreach (\array_keys($headers) as $headerKey) { + if (\strcasecmp($headerKey, $key) === 0) { + $explicitHeaderExists = true; + break; + } + } + if (!$explicitHeaderExists) { + $headers[$key] = $value; + } + } + + return $this->transaction->send( + new Request($method, $url, $headers, $body, $this->protocolVersion) + ); + } +} diff --git a/vendor/react/http/src/Client/Client.php b/vendor/react/http/src/Client/Client.php new file mode 100644 index 0000000..7a5180a --- /dev/null +++ b/vendor/react/http/src/Client/Client.php @@ -0,0 +1,27 @@ +connectionManager = $connectionManager; + } + + /** @return ClientRequestStream */ + public function request(RequestInterface $request) + { + return new ClientRequestStream($this->connectionManager, $request); + } +} diff --git a/vendor/react/http/src/HttpServer.php b/vendor/react/http/src/HttpServer.php new file mode 100644 index 0000000..f233473 --- /dev/null +++ b/vendor/react/http/src/HttpServer.php @@ -0,0 +1,351 @@ + 'text/plain' + * ), + * "Hello World!\n" + * ); + * }); + * ``` + * + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#server-request) chapter for more details. + * + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#server-response) chapter for more details. + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * In order to start listening for any incoming connections, the `HttpServer` needs + * to be attached to an instance of + * [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * through the [`listen()`](#listen) method as described in the following + * chapter. In its most simple form, you can attach this to a + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $http = new React\Http\HttpServer($handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); + * $http->listen($socket); + * ``` + * + * See also the [`listen()`](#listen) method and + * [hello world server example](../examples/51-server-hello-world.php) + * for more details. + * + * By default, the `HttpServer` buffers and parses the complete incoming HTTP + * request in memory. It will invoke the given request handler function when the + * complete request headers and request body has been received. This means the + * [request](#server-request) object passed to your request handler function will be + * fully compatible with PSR-7 (http-message). This provides sane defaults for + * 80% of the use cases and is the recommended way to use this library unless + * you're sure you know what you're doing. + * + * On the other hand, buffering complete HTTP requests in memory until they can + * be processed by your request handler function means that this class has to + * employ a number of limits to avoid consuming too much memory. In order to + * take the more advanced configuration out your hand, it respects setting from + * your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its + * default settings. This is a list of PHP settings this class respects with + * their respective default values: + * + * ``` + * memory_limit 128M + * post_max_size 8M // capped at 64K + * + * enable_post_data_reading 1 + * max_input_nesting_level 64 + * max_input_vars 1000 + * + * file_uploads 1 + * upload_max_filesize 2M + * max_file_uploads 20 + * ``` + * + * In particular, the `post_max_size` setting limits how much memory a single + * HTTP request is allowed to consume while buffering its request body. This + * needs to be limited because the server can process a large number of requests + * concurrently, so the server may potentially consume a large amount of memory + * otherwise. To support higher concurrency by default, this value is capped + * at `64K`. If you assign a higher value, it will only allow `64K` by default. + * If a request exceeds this limit, its request body will be ignored and it will + * be processed like a request with no request body at all. See below for + * explicit configuration to override this setting. + * + * By default, this class will try to avoid consuming more than half of your + * `memory_limit` for buffering multiple concurrent HTTP requests. As such, with + * the above default settings of `128M` max, it will try to consume no more than + * `64M` for buffering multiple concurrent HTTP requests. As a consequence, it + * will limit the concurrency to `1024` HTTP requests with the above defaults. + * + * It is imperative that you assign reasonable values to your PHP ini settings. + * It is usually recommended to not support buffering incoming HTTP requests + * with a large HTTP request body (e.g. large file uploads). If you want to + * increase this buffer size, you will have to also increase the total memory + * limit to allow for more concurrent requests (set `memory_limit 512M` or more) + * or explicitly limit concurrency. + * + * In order to override the above buffering defaults, you can configure the + * `HttpServer` explicitly. You can use the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once like this: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * )); + * ``` + * + * In this example, we allow processing up to 100 concurrent requests at once + * and each request can buffer up to `2M`. This means you may have to keep a + * maximum of `200M` of memory for incoming request body buffers. Accordingly, + * you need to adjust the `memory_limit` ini setting to allow for these buffers + * plus your actual application logic memory requirements (think `512M` or more). + * + * > Internally, this class automatically assigns these middleware handlers + * automatically when no [`StreamingRequestMiddleware`](#streamingrequestmiddleware) + * is given. Accordingly, you can use this example to override all default + * settings to implement custom limits. + * + * As an alternative to buffering the complete request body in memory, you can + * also use a streaming approach where only small chunks of data have to be kept + * in memory: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * $handler + * ); + * ``` + * + * In this case, it will invoke the request handler function once the HTTP + * request headers have been received, i.e. before receiving the potentially + * much larger HTTP request body. This means the [request](#server-request) passed to + * your request handler function may not be fully compatible with PSR-7. This is + * specifically designed to help with more advanced use cases where you want to + * have full control over consuming the incoming HTTP request body and + * concurrency settings. See also [streaming incoming request](#streaming-incoming-request) + * below for more details. + * + * > Changelog v1.5.0: This class has been renamed to `HttpServer` from the + * previous `Server` class in order to avoid any ambiguities. + * The previous name has been deprecated and should not be used anymore. + */ +final class HttpServer extends EventEmitter +{ + /** + * The maximum buffer size used for each request. + * + * This needs to be limited because the server can process a large number of + * requests concurrently, so the server may potentially consume a large + * amount of memory otherwise. + * + * See `RequestBodyBufferMiddleware` to override this setting. + * + * @internal + */ + const MAXIMUM_BUFFER_SIZE = 65536; // 64 KiB + + /** + * @var StreamingServer + */ + private $streamingServer; + + /** + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param callable|LoopInterface $requestHandlerOrLoop + * @param callable[] ...$requestHandler + * @see self::listen() + */ + public function __construct($requestHandlerOrLoop) + { + $requestHandlers = \func_get_args(); + if (reset($requestHandlers) instanceof LoopInterface) { + $loop = \array_shift($requestHandlers); + } else { + $loop = Loop::get(); + } + + $requestHandlersCount = \count($requestHandlers); + if ($requestHandlersCount === 0 || \count(\array_filter($requestHandlers, 'is_callable')) < $requestHandlersCount) { + throw new \InvalidArgumentException('Invalid request handler given'); + } + + $streaming = false; + foreach ((array) $requestHandlers as $handler) { + if ($handler instanceof StreamingRequestMiddleware) { + $streaming = true; + break; + } + } + + $middleware = array(); + if (!$streaming) { + $maxSize = $this->getMaxRequestSize(); + $concurrency = $this->getConcurrentRequestsLimit(\ini_get('memory_limit'), $maxSize); + if ($concurrency !== null) { + $middleware[] = new LimitConcurrentRequestsMiddleware($concurrency); + } + $middleware[] = new RequestBodyBufferMiddleware($maxSize); + // Checking for an empty string because that is what a boolean + // false is returned as by ini_get depending on the PHP version. + // @link http://php.net/manual/en/ini.core.php#ini.enable-post-data-reading + // @link http://php.net/manual/en/function.ini-get.php#refsect1-function.ini-get-notes + // @link https://3v4l.org/qJtsa + $enablePostDataReading = \ini_get('enable_post_data_reading'); + if ($enablePostDataReading !== '') { + $middleware[] = new RequestBodyParserMiddleware(); + } + } + + $middleware = \array_merge($middleware, $requestHandlers); + + /** + * Filter out any configuration middleware, no need to run requests through something that isn't + * doing anything with the request. + */ + $middleware = \array_filter($middleware, function ($handler) { + return !($handler instanceof StreamingRequestMiddleware); + }); + + $this->streamingServer = new StreamingServer($loop, new MiddlewareRunner($middleware)); + + $that = $this; + $this->streamingServer->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + } + + /** + * Starts listening for HTTP requests on the given socket server instance + * + * The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface) + * is responsible for emitting the underlying streaming connections. This + * HTTP server needs to be attached to it in order to process any + * connections and pase incoming streaming data as incoming HTTP request + * messages. In its most common form, you can attach this to a + * [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $http = new React\Http\HttpServer($handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080'); + * $http->listen($socket); + * ``` + * + * See also [hello world server example](../examples/51-server-hello-world.php) + * for more details. + * + * This example will start listening for HTTP requests on the alternative + * HTTP port `8080` on all interfaces (publicly). As an alternative, it is + * very common to use a reverse proxy and let this HTTP server listen on the + * localhost (loopback) interface only by using the listen address + * `127.0.0.1:8080` instead. This way, you host your application(s) on the + * default HTTP port `80` and only route specific requests to this HTTP + * server. + * + * Likewise, it's usually recommended to use a reverse proxy setup to accept + * secure HTTPS requests on default HTTPS port `443` (TLS termination) and + * only route plaintext requests to this HTTP server. As an alternative, you + * can also accept secure HTTPS requests with this HTTP server by attaching + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * using a secure TLS listen address, a certificate file and optional + * `passphrase` like this: + * + * ```php + * $http = new React\Http\HttpServer($handler); + * + * $socket = new React\Socket\SocketServer('tls://0.0.0.0:8443', array( + * 'tls' => array( + * 'local_cert' => __DIR__ . '/localhost.pem' + * ) + * )); + * $http->listen($socket); + * ``` + * + * See also [hello world HTTPS example](../examples/61-server-hello-world-https.php) + * for more details. + * + * @param ServerInterface $socket + */ + public function listen(ServerInterface $socket) + { + $this->streamingServer->listen($socket); + } + + /** + * @param string $memory_limit + * @param string $post_max_size + * @return ?int + */ + private function getConcurrentRequestsLimit($memory_limit, $post_max_size) + { + if ($memory_limit == -1) { + return null; + } + + $availableMemory = IniUtil::iniSizeToBytes($memory_limit) / 2; + $concurrentRequests = (int) \ceil($availableMemory / IniUtil::iniSizeToBytes($post_max_size)); + + return $concurrentRequests; + } + + /** + * @param ?string $post_max_size + * @return int + */ + private function getMaxRequestSize($post_max_size = null) + { + $maxSize = IniUtil::iniSizeToBytes($post_max_size === null ? \ini_get('post_max_size') : $post_max_size); + + return ($maxSize === 0 || $maxSize >= self::MAXIMUM_BUFFER_SIZE) ? self::MAXIMUM_BUFFER_SIZE : $maxSize; + } +} diff --git a/vendor/react/http/src/Io/AbstractMessage.php b/vendor/react/http/src/Io/AbstractMessage.php new file mode 100644 index 0000000..a0706bb --- /dev/null +++ b/vendor/react/http/src/Io/AbstractMessage.php @@ -0,0 +1,172 @@ +@,;:\\\"\/\[\]?={}\x00-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m'; + + /** @var array */ + private $headers = array(); + + /** @var array */ + private $headerNamesLowerCase = array(); + + /** @var string */ + private $protocolVersion; + + /** @var StreamInterface */ + private $body; + + /** + * @param string $protocolVersion + * @param array $headers + * @param StreamInterface $body + */ + protected function __construct($protocolVersion, array $headers, StreamInterface $body) + { + foreach ($headers as $name => $value) { + if ($value !== array()) { + if (\is_array($value)) { + foreach ($value as &$one) { + $one = (string) $one; + } + } else { + $value = array((string) $value); + } + + $lower = \strtolower($name); + if (isset($this->headerNamesLowerCase[$lower])) { + $value = \array_merge($this->headers[$this->headerNamesLowerCase[$lower]], $value); + unset($this->headers[$this->headerNamesLowerCase[$lower]]); + } + + $this->headers[$name] = $value; + $this->headerNamesLowerCase[$lower] = $name; + } + } + + $this->protocolVersion = (string) $protocolVersion; + $this->body = $body; + } + + public function getProtocolVersion() + { + return $this->protocolVersion; + } + + public function withProtocolVersion($version) + { + if ((string) $version === $this->protocolVersion) { + return $this; + } + + $message = clone $this; + $message->protocolVersion = (string) $version; + + return $message; + } + + public function getHeaders() + { + return $this->headers; + } + + public function hasHeader($name) + { + return isset($this->headerNamesLowerCase[\strtolower($name)]); + } + + public function getHeader($name) + { + $lower = \strtolower($name); + return isset($this->headerNamesLowerCase[$lower]) ? $this->headers[$this->headerNamesLowerCase[$lower]] : array(); + } + + public function getHeaderLine($name) + { + return \implode(', ', $this->getHeader($name)); + } + + public function withHeader($name, $value) + { + if ($value === array()) { + return $this->withoutHeader($name); + } elseif (\is_array($value)) { + foreach ($value as &$one) { + $one = (string) $one; + } + } else { + $value = array((string) $value); + } + + $lower = \strtolower($name); + if (isset($this->headerNamesLowerCase[$lower]) && $this->headerNamesLowerCase[$lower] === (string) $name && $this->headers[$this->headerNamesLowerCase[$lower]] === $value) { + return $this; + } + + $message = clone $this; + if (isset($message->headerNamesLowerCase[$lower])) { + unset($message->headers[$message->headerNamesLowerCase[$lower]]); + } + + $message->headers[$name] = $value; + $message->headerNamesLowerCase[$lower] = $name; + + return $message; + } + + public function withAddedHeader($name, $value) + { + if ($value === array()) { + return $this; + } + + return $this->withHeader($name, \array_merge($this->getHeader($name), \is_array($value) ? $value : array($value))); + } + + public function withoutHeader($name) + { + $lower = \strtolower($name); + if (!isset($this->headerNamesLowerCase[$lower])) { + return $this; + } + + $message = clone $this; + unset($message->headers[$message->headerNamesLowerCase[$lower]], $message->headerNamesLowerCase[$lower]); + + return $message; + } + + public function getBody() + { + return $this->body; + } + + public function withBody(StreamInterface $body) + { + if ($body === $this->body) { + return $this; + } + + $message = clone $this; + $message->body = $body; + + return $message; + } +} diff --git a/vendor/react/http/src/Io/AbstractRequest.php b/vendor/react/http/src/Io/AbstractRequest.php new file mode 100644 index 0000000..f32307f --- /dev/null +++ b/vendor/react/http/src/Io/AbstractRequest.php @@ -0,0 +1,156 @@ + $headers + * @param StreamInterface $body + * @param string unknown $protocolVersion + */ + protected function __construct( + $method, + $uri, + array $headers, + StreamInterface $body, + $protocolVersion + ) { + if (\is_string($uri)) { + $uri = new Uri($uri); + } elseif (!$uri instanceof UriInterface) { + throw new \InvalidArgumentException( + 'Argument #2 ($uri) expected string|Psr\Http\Message\UriInterface' + ); + } + + // assign default `Host` request header from URI unless already given explicitly + $host = $uri->getHost(); + if ($host !== '') { + foreach ($headers as $name => $value) { + if (\strtolower($name) === 'host' && $value !== array()) { + $host = ''; + break; + } + } + if ($host !== '') { + $port = $uri->getPort(); + if ($port !== null && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) { + $host .= ':' . $port; + } + + $headers = array('Host' => $host) + $headers; + } + } + + parent::__construct($protocolVersion, $headers, $body); + + $this->method = $method; + $this->uri = $uri; + } + + public function getRequestTarget() + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target === '') { + $target = '/'; + } + if (($query = $this->uri->getQuery()) !== '') { + $target .= '?' . $query; + } + + return $target; + } + + public function withRequestTarget($requestTarget) + { + if ((string) $requestTarget === $this->requestTarget) { + return $this; + } + + $request = clone $this; + $request->requestTarget = (string) $requestTarget; + + return $request; + } + + public function getMethod() + { + return $this->method; + } + + public function withMethod($method) + { + if ((string) $method === $this->method) { + return $this; + } + + $request = clone $this; + $request->method = (string) $method; + + return $request; + } + + public function getUri() + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false) + { + if ($uri === $this->uri) { + return $this; + } + + $request = clone $this; + $request->uri = $uri; + + $host = $uri->getHost(); + $port = $uri->getPort(); + if ($port !== null && $host !== '' && (!($port === 80 && $uri->getScheme() === 'http') || !($port === 443 && $uri->getScheme() === 'https'))) { + $host .= ':' . $port; + } + + // update `Host` request header if URI contains a new host and `$preserveHost` is false + if ($host !== '' && (!$preserveHost || $request->getHeaderLine('Host') === '')) { + // first remove all headers before assigning `Host` header to ensure it always comes first + foreach (\array_keys($request->getHeaders()) as $name) { + $request = $request->withoutHeader($name); + } + + // add `Host` header first, then all other original headers + $request = $request->withHeader('Host', $host); + foreach ($this->withoutHeader('Host')->getHeaders() as $name => $value) { + $request = $request->withHeader($name, $value); + } + } + + return $request; + } +} diff --git a/vendor/react/http/src/Io/BufferedBody.php b/vendor/react/http/src/Io/BufferedBody.php new file mode 100644 index 0000000..4a4d839 --- /dev/null +++ b/vendor/react/http/src/Io/BufferedBody.php @@ -0,0 +1,179 @@ +buffer = $buffer; + } + + public function __toString() + { + if ($this->closed) { + return ''; + } + + $this->seek(0); + + return $this->getContents(); + } + + public function close() + { + $this->buffer = ''; + $this->position = 0; + $this->closed = true; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize() + { + return $this->closed ? null : \strlen($this->buffer); + } + + public function tell() + { + if ($this->closed) { + throw new \RuntimeException('Unable to tell position of closed stream'); + } + + return $this->position; + } + + public function eof() + { + return $this->position >= \strlen($this->buffer); + } + + public function isSeekable() + { + return !$this->closed; + } + + public function seek($offset, $whence = \SEEK_SET) + { + if ($this->closed) { + throw new \RuntimeException('Unable to seek on closed stream'); + } + + $old = $this->position; + + if ($whence === \SEEK_SET) { + $this->position = $offset; + } elseif ($whence === \SEEK_CUR) { + $this->position += $offset; + } elseif ($whence === \SEEK_END) { + $this->position = \strlen($this->buffer) + $offset; + } else { + throw new \InvalidArgumentException('Invalid seek mode given'); + } + + if (!\is_int($this->position) || $this->position < 0) { + $this->position = $old; + throw new \RuntimeException('Unable to seek to position'); + } + } + + public function rewind() + { + $this->seek(0); + } + + public function isWritable() + { + return !$this->closed; + } + + public function write($string) + { + if ($this->closed) { + throw new \RuntimeException('Unable to write to closed stream'); + } + + if ($string === '') { + return 0; + } + + if ($this->position > 0 && !isset($this->buffer[$this->position - 1])) { + $this->buffer = \str_pad($this->buffer, $this->position, "\0"); + } + + $len = \strlen($string); + $this->buffer = \substr($this->buffer, 0, $this->position) . $string . \substr($this->buffer, $this->position + $len); + $this->position += $len; + + return $len; + } + + public function isReadable() + { + return !$this->closed; + } + + public function read($length) + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if ($length < 1) { + throw new \InvalidArgumentException('Invalid read length given'); + } + + if ($this->position + $length > \strlen($this->buffer)) { + $length = \strlen($this->buffer) - $this->position; + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position += $length; + + return \substr($this->buffer, $pos, $length); + } + + public function getContents() + { + if ($this->closed) { + throw new \RuntimeException('Unable to read from closed stream'); + } + + if (!isset($this->buffer[$this->position])) { + return ''; + } + + $pos = $this->position; + $this->position = \strlen($this->buffer); + + return \substr($this->buffer, $pos); + } + + public function getMetadata($key = null) + { + return $key === null ? array() : null; + } +} diff --git a/vendor/react/http/src/Io/ChunkedDecoder.php b/vendor/react/http/src/Io/ChunkedDecoder.php new file mode 100644 index 0000000..2f58f42 --- /dev/null +++ b/vendor/react/http/src/Io/ChunkedDecoder.php @@ -0,0 +1,175 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->buffer = ''; + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new Exception('Unexpected end event')); + } + } + + /** @internal */ + public function handleError(Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + while ($this->buffer !== '') { + if (!$this->headerCompleted) { + $positionCrlf = \strpos($this->buffer, static::CRLF); + + if ($positionCrlf === false) { + // Header shouldn't be bigger than 1024 bytes + if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) { + $this->handleError(new Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes')); + } + return; + } + + $header = \strtolower((string)\substr($this->buffer, 0, $positionCrlf)); + $hexValue = $header; + + if (\strpos($header, ';') !== false) { + $array = \explode(';', $header); + $hexValue = $array[0]; + } + + if ($hexValue !== '') { + $hexValue = \ltrim(\trim($hexValue), "0"); + if ($hexValue === '') { + $hexValue = "0"; + } + } + + $this->chunkSize = @\hexdec($hexValue); + if (!\is_int($this->chunkSize) || \dechex($this->chunkSize) !== $hexValue) { + $this->handleError(new Exception($hexValue . ' is not a valid hexadecimal number')); + return; + } + + $this->buffer = (string)\substr($this->buffer, $positionCrlf + 2); + $this->headerCompleted = true; + if ($this->buffer === '') { + return; + } + } + + $chunk = (string)\substr($this->buffer, 0, $this->chunkSize - $this->transferredSize); + + if ($chunk !== '') { + $this->transferredSize += \strlen($chunk); + $this->emit('data', array($chunk)); + $this->buffer = (string)\substr($this->buffer, \strlen($chunk)); + } + + $positionCrlf = \strpos($this->buffer, static::CRLF); + + if ($positionCrlf === 0) { + if ($this->chunkSize === 0) { + $this->emit('end'); + $this->close(); + return; + } + $this->chunkSize = 0; + $this->headerCompleted = false; + $this->transferredSize = 0; + $this->buffer = (string)\substr($this->buffer, 2); + } elseif ($this->chunkSize === 0) { + // end chunk received, skip all trailer data + $this->buffer = (string)\substr($this->buffer, $positionCrlf); + } + + if ($positionCrlf !== 0 && $this->chunkSize !== 0 && $this->chunkSize === $this->transferredSize && \strlen($this->buffer) > 2) { + // the first 2 characters are not CRLF, send error event + $this->handleError(new Exception('Chunk does not end with a CRLF')); + return; + } + + if ($positionCrlf !== 0 && \strlen($this->buffer) < 2) { + // No CRLF found, wait for additional data which could be a CRLF + return; + } + } + } +} diff --git a/vendor/react/http/src/Io/ChunkedEncoder.php b/vendor/react/http/src/Io/ChunkedEncoder.php new file mode 100644 index 0000000..c84ef54 --- /dev/null +++ b/vendor/react/http/src/Io/ChunkedEncoder.php @@ -0,0 +1,92 @@ +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/react/http/src/Io/ClientConnectionManager.php b/vendor/react/http/src/Io/ClientConnectionManager.php new file mode 100644 index 0000000..faac98b --- /dev/null +++ b/vendor/react/http/src/Io/ClientConnectionManager.php @@ -0,0 +1,137 @@ +connector = $connector; + $this->loop = $loop; + } + + /** + * @return PromiseInterface + */ + public function connect(UriInterface $uri) + { + $scheme = $uri->getScheme(); + if ($scheme !== 'https' && $scheme !== 'http') { + return \React\Promise\reject(new \InvalidArgumentException( + 'Invalid request URL given' + )); + } + + $port = $uri->getPort(); + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + $uri = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port; + + // Reuse idle connection for same URI if available + foreach ($this->idleConnections as $id => $connection) { + if ($this->idleUris[$id] === $uri) { + assert($this->idleStreamHandlers[$id] instanceof \Closure); + $connection->removeListener('close', $this->idleStreamHandlers[$id]); + $connection->removeListener('data', $this->idleStreamHandlers[$id]); + $connection->removeListener('error', $this->idleStreamHandlers[$id]); + + assert($this->idleTimers[$id] instanceof TimerInterface); + $this->loop->cancelTimer($this->idleTimers[$id]); + unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]); + + return \React\Promise\resolve($connection); + } + } + + // Create new connection if no idle connection to same URI is available + return $this->connector->connect($uri); + } + + /** + * Hands back an idle connection to the connection manager for possible future reuse. + * + * @return void + */ + public function keepAlive(UriInterface $uri, ConnectionInterface $connection) + { + $scheme = $uri->getScheme(); + assert($scheme === 'https' || $scheme === 'http'); + + $port = $uri->getPort(); + if ($port === null) { + $port = $scheme === 'https' ? 443 : 80; + } + + $this->idleUris[] = ($scheme === 'https' ? 'tls://' : '') . $uri->getHost() . ':' . $port; + $this->idleConnections[] = $connection; + + $that = $this; + $cleanUp = function () use ($connection, $that) { + // call public method to support legacy PHP 5.3 + $that->cleanUpConnection($connection); + }; + + // clean up and close connection when maximum time to keep-alive idle connection has passed + $this->idleTimers[] = $this->loop->addTimer($this->maximumTimeToKeepAliveIdleConnection, $cleanUp); + + // clean up and close connection when unexpected close/data/error event happens during idle time + $this->idleStreamHandlers[] = $cleanUp; + $connection->on('close', $cleanUp); + $connection->on('data', $cleanUp); + $connection->on('error', $cleanUp); + } + + /** + * @internal + * @return void + */ + public function cleanUpConnection(ConnectionInterface $connection) // private (PHP 5.4+) + { + $id = \array_search($connection, $this->idleConnections, true); + if ($id === false) { + return; + } + + assert(\is_int($id)); + assert($this->idleTimers[$id] instanceof TimerInterface); + $this->loop->cancelTimer($this->idleTimers[$id]); + unset($this->idleUris[$id], $this->idleConnections[$id], $this->idleTimers[$id], $this->idleStreamHandlers[$id]); + + $connection->close(); + } +} diff --git a/vendor/react/http/src/Io/ClientRequestState.php b/vendor/react/http/src/Io/ClientRequestState.php new file mode 100644 index 0000000..73a63a1 --- /dev/null +++ b/vendor/react/http/src/Io/ClientRequestState.php @@ -0,0 +1,16 @@ +connectionManager = $connectionManager; + $this->request = $request; + } + + public function isWritable() + { + return self::STATE_END > $this->state && !$this->ended; + } + + private function writeHead() + { + $this->state = self::STATE_WRITING_HEAD; + + $expected = 0; + $headers = "{$this->request->getMethod()} {$this->request->getRequestTarget()} HTTP/{$this->request->getProtocolVersion()}\r\n"; + foreach ($this->request->getHeaders() as $name => $values) { + if (\strpos($name, ':') !== false) { + $expected = -1; + break; + } + foreach ($values as $value) { + $headers .= "$name: $value\r\n"; + ++$expected; + } + } + + /** @var array $m legacy PHP 5.3 only */ + if (!\preg_match('#^\S+ \S+ HTTP/1\.[01]\r\n#m', $headers) || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) { + $this->closeError(new \InvalidArgumentException('Unable to send request with invalid request headers')); + return; + } + + $connectionRef = &$this->connection; + $stateRef = &$this->state; + $pendingWrites = &$this->pendingWrites; + $that = $this; + + $promise = $this->connectionManager->connect($this->request->getUri()); + $promise->then( + function (ConnectionInterface $connection) use ($headers, &$connectionRef, &$stateRef, &$pendingWrites, $that) { + $connectionRef = $connection; + assert($connectionRef instanceof ConnectionInterface); + + $connection->on('drain', array($that, 'handleDrain')); + $connection->on('data', array($that, 'handleData')); + $connection->on('end', array($that, 'handleEnd')); + $connection->on('error', array($that, 'handleError')); + $connection->on('close', array($that, 'close')); + + $more = $connection->write($headers . "\r\n" . $pendingWrites); + + assert($stateRef === ClientRequestStream::STATE_WRITING_HEAD); + $stateRef = ClientRequestStream::STATE_HEAD_WRITTEN; + + // clear pending writes if non-empty + if ($pendingWrites !== '') { + $pendingWrites = ''; + + if ($more) { + $that->emit('drain'); + } + } + }, + array($this, 'closeError') + ); + + $this->on('close', function() use ($promise) { + $promise->cancel(); + }); + } + + public function write($data) + { + if (!$this->isWritable()) { + return false; + } + + // write directly to connection stream if already available + if (self::STATE_HEAD_WRITTEN <= $this->state) { + return $this->connection->write($data); + } + + // otherwise buffer and try to establish connection + $this->pendingWrites .= $data; + if (self::STATE_WRITING_HEAD > $this->state) { + $this->writeHead(); + } + + return false; + } + + public function end($data = null) + { + if (!$this->isWritable()) { + return; + } + + if (null !== $data) { + $this->write($data); + } else if (self::STATE_WRITING_HEAD > $this->state) { + $this->writeHead(); + } + + $this->ended = true; + } + + /** @internal */ + public function handleDrain() + { + $this->emit('drain'); + } + + /** @internal */ + public function handleData($data) + { + $this->buffer .= $data; + + // buffer until double CRLF (or double LF for compatibility with legacy servers) + $eom = \strpos($this->buffer, "\r\n\r\n"); + $eomLegacy = \strpos($this->buffer, "\n\n"); + if ($eom !== false || $eomLegacy !== false) { + try { + if ($eom !== false && ($eomLegacy === false || $eom < $eomLegacy)) { + $response = Response::parseMessage(\substr($this->buffer, 0, $eom + 2)); + $bodyChunk = (string) \substr($this->buffer, $eom + 4); + } else { + $response = Response::parseMessage(\substr($this->buffer, 0, $eomLegacy + 1)); + $bodyChunk = (string) \substr($this->buffer, $eomLegacy + 2); + } + } catch (\InvalidArgumentException $exception) { + $this->closeError($exception); + return; + } + + // response headers successfully received => remove listeners for connection events + $connection = $this->connection; + assert($connection instanceof ConnectionInterface); + $connection->removeListener('drain', array($this, 'handleDrain')); + $connection->removeListener('data', array($this, 'handleData')); + $connection->removeListener('end', array($this, 'handleEnd')); + $connection->removeListener('error', array($this, 'handleError')); + $connection->removeListener('close', array($this, 'close')); + $this->connection = null; + $this->buffer = ''; + + // take control over connection handling and check if we can reuse the connection once response body closes + $that = $this; + $request = $this->request; + $connectionManager = $this->connectionManager; + $successfulEndReceived = false; + $input = $body = new CloseProtectionStream($connection); + $input->on('close', function () use ($connection, $that, $connectionManager, $request, $response, &$successfulEndReceived) { + // only reuse connection after successful response and both request and response allow keep alive + if ($successfulEndReceived && $connection->isReadable() && $that->hasMessageKeepAliveEnabled($response) && $that->hasMessageKeepAliveEnabled($request)) { + $connectionManager->keepAlive($request->getUri(), $connection); + } else { + $connection->close(); + } + + $that->close(); + }); + + // determine length of response body + $length = null; + $code = $response->getStatusCode(); + if ($this->request->getMethod() === 'HEAD' || ($code >= 100 && $code < 200) || $code == Response::STATUS_NO_CONTENT || $code == Response::STATUS_NOT_MODIFIED) { + $length = 0; + } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $body = new ChunkedDecoder($body); + } elseif ($response->hasHeader('Content-Length')) { + $length = (int) $response->getHeaderLine('Content-Length'); + } + $response = $response->withBody($body = new ReadableBodyStream($body, $length)); + $body->on('end', function () use (&$successfulEndReceived) { + $successfulEndReceived = true; + }); + + // emit response with streaming response body (see `Sender`) + $this->emit('response', array($response, $body)); + + // re-emit HTTP response body to trigger body parsing if parts of it are buffered + if ($bodyChunk !== '') { + $input->handleData($bodyChunk); + } elseif ($length === 0) { + $input->handleEnd(); + } + } + } + + /** @internal */ + public function handleEnd() + { + $this->closeError(new \RuntimeException( + "Connection ended before receiving response" + )); + } + + /** @internal */ + public function handleError(\Exception $error) + { + $this->closeError(new \RuntimeException( + "An error occurred in the underlying stream", + 0, + $error + )); + } + + /** @internal */ + public function closeError(\Exception $error) + { + if (self::STATE_END <= $this->state) { + return; + } + $this->emit('error', array($error)); + $this->close(); + } + + public function close() + { + if (self::STATE_END <= $this->state) { + return; + } + + $this->state = self::STATE_END; + $this->pendingWrites = ''; + $this->buffer = ''; + + if ($this->connection instanceof ConnectionInterface) { + $this->connection->close(); + $this->connection = null; + } + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** + * @internal + * @return bool + * @link https://www.rfc-editor.org/rfc/rfc9112#section-9.3 + * @link https://www.rfc-editor.org/rfc/rfc7230#section-6.1 + */ + public function hasMessageKeepAliveEnabled(MessageInterface $message) + { + // @link https://www.rfc-editor.org/rfc/rfc9110#section-7.6.1 + $connectionOptions = \array_map('trim', \explode(',', \strtolower($message->getHeaderLine('Connection')))); + + if (\in_array('close', $connectionOptions, true)) { + return false; + } + + if ($message->getProtocolVersion() === '1.1') { + return true; + } + + if (\in_array('keep-alive', $connectionOptions, true)) { + return true; + } + + return false; + } +} diff --git a/vendor/react/http/src/Io/Clock.php b/vendor/react/http/src/Io/Clock.php new file mode 100644 index 0000000..92c1cb0 --- /dev/null +++ b/vendor/react/http/src/Io/Clock.php @@ -0,0 +1,54 @@ +loop = $loop; + } + + /** @return float */ + public function now() + { + if ($this->now === null) { + $this->now = \microtime(true); + + // remember clock for current loop tick only and update on next tick + $now =& $this->now; + $this->loop->futureTick(function () use (&$now) { + assert($now !== null); + $now = null; + }); + } + + return $this->now; + } +} diff --git a/vendor/react/http/src/Io/CloseProtectionStream.php b/vendor/react/http/src/Io/CloseProtectionStream.php new file mode 100644 index 0000000..2e1ed6e --- /dev/null +++ b/vendor/react/http/src/Io/CloseProtectionStream.php @@ -0,0 +1,111 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->paused = true; + $this->input->pause(); + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + // stop listening for incoming events + $this->input->removeListener('data', array($this, 'handleData')); + $this->input->removeListener('error', array($this, 'handleError')); + $this->input->removeListener('end', array($this, 'handleEnd')); + $this->input->removeListener('close', array($this, 'close')); + + // resume the stream to ensure we discard everything from incoming connection + if ($this->paused) { + $this->paused = false; + $this->input->resume(); + } + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleEnd() + { + $this->emit('end'); + $this->close(); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + } +} diff --git a/vendor/react/http/src/Io/EmptyBodyStream.php b/vendor/react/http/src/Io/EmptyBodyStream.php new file mode 100644 index 0000000..5056219 --- /dev/null +++ b/vendor/react/http/src/Io/EmptyBodyStream.php @@ -0,0 +1,142 @@ +closed; + } + + public function pause() + { + // NOOP + } + + public function resume() + { + // NOOP + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return 0; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return ($key === null) ? array() : null; + } +} diff --git a/vendor/react/http/src/Io/HttpBodyStream.php b/vendor/react/http/src/Io/HttpBodyStream.php new file mode 100644 index 0000000..25d15a1 --- /dev/null +++ b/vendor/react/http/src/Io/HttpBodyStream.php @@ -0,0 +1,182 @@ +input = $input; + $this->size = $size; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->input->isReadable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + public function getSize() + { + return $this->size; + } + + /** @ignore */ + public function __toString() + { + return ''; + } + + /** @ignore */ + public function detach() + { + return null; + } + + /** @ignore */ + public function tell() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function eof() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isSeekable() + { + return false; + } + + /** @ignore */ + public function seek($offset, $whence = SEEK_SET) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function rewind() + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function isWritable() + { + return false; + } + + /** @ignore */ + public function write($string) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function read($length) + { + throw new \BadMethodCallException(); + } + + /** @ignore */ + public function getContents() + { + return ''; + } + + /** @ignore */ + public function getMetadata($key = null) + { + return null; + } + + /** @internal */ + public function handleData($data) + { + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } +} diff --git a/vendor/react/http/src/Io/IniUtil.php b/vendor/react/http/src/Io/IniUtil.php new file mode 100644 index 0000000..612aae2 --- /dev/null +++ b/vendor/react/http/src/Io/IniUtil.php @@ -0,0 +1,48 @@ +stream = $stream; + $this->maxLength = $maxLength; + + $this->stream->on('data', array($this, 'handleData')); + $this->stream->on('end', array($this, 'handleEnd')); + $this->stream->on('error', array($this, 'handleError')); + $this->stream->on('close', array($this, 'close')); + } + + public function isReadable() + { + return !$this->closed && $this->stream->isReadable(); + } + + public function pause() + { + $this->stream->pause(); + } + + public function resume() + { + $this->stream->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + + $this->stream->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if (($this->transferredLength + \strlen($data)) > $this->maxLength) { + // Only emit data until the value of 'Content-Length' is reached, the rest will be ignored + $data = (string)\substr($data, 0, $this->maxLength - $this->transferredLength); + } + + if ($data !== '') { + $this->transferredLength += \strlen($data); + $this->emit('data', array($data)); + } + + if ($this->transferredLength === $this->maxLength) { + // 'Content-Length' reached, stream will end + $this->emit('end'); + $this->close(); + $this->stream->removeListener('data', array($this, 'handleData')); + } + } + + /** @internal */ + public function handleError(\Exception $e) + { + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if (!$this->closed) { + $this->handleError(new \Exception('Unexpected end event')); + } + } + +} diff --git a/vendor/react/http/src/Io/MiddlewareRunner.php b/vendor/react/http/src/Io/MiddlewareRunner.php new file mode 100644 index 0000000..dedf6ff --- /dev/null +++ b/vendor/react/http/src/Io/MiddlewareRunner.php @@ -0,0 +1,61 @@ +middleware = \array_values($middleware); + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface|PromiseInterface + * @throws \Exception + */ + public function __invoke(ServerRequestInterface $request) + { + if (empty($this->middleware)) { + throw new \RuntimeException('No middleware to run'); + } + + return $this->call($request, 0); + } + + /** @internal */ + public function call(ServerRequestInterface $request, $position) + { + // final request handler will be invoked without a next handler + if (!isset($this->middleware[$position + 1])) { + $handler = $this->middleware[$position]; + return $handler($request); + } + + $that = $this; + $next = function (ServerRequestInterface $request) use ($that, $position) { + return $that->call($request, $position + 1); + }; + + // invoke middleware request handler with next handler + $handler = $this->middleware[$position]; + return $handler($request, $next); + } +} diff --git a/vendor/react/http/src/Io/MultipartParser.php b/vendor/react/http/src/Io/MultipartParser.php new file mode 100644 index 0000000..539107a --- /dev/null +++ b/vendor/react/http/src/Io/MultipartParser.php @@ -0,0 +1,345 @@ +maxInputVars = (int)$var; + } + $var = \ini_get('max_input_nesting_level'); + if ($var !== false) { + $this->maxInputNestingLevel = (int)$var; + } + + if ($uploadMaxFilesize === null) { + $uploadMaxFilesize = \ini_get('upload_max_filesize'); + } + + $this->uploadMaxFilesize = IniUtil::iniSizeToBytes($uploadMaxFilesize); + $this->maxFileUploads = $maxFileUploads === null ? (\ini_get('file_uploads') === '' ? 0 : (int)\ini_get('max_file_uploads')) : (int)$maxFileUploads; + + $this->maxMultipartBodyParts = $this->maxInputVars + $this->maxFileUploads; + } + + public function parse(ServerRequestInterface $request) + { + $contentType = $request->getHeaderLine('content-type'); + if(!\preg_match('/boundary="?(.*?)"?$/', $contentType, $matches)) { + return $request; + } + + $this->request = $request; + $this->parseBody('--' . $matches[1], (string)$request->getBody()); + + $request = $this->request; + $this->request = null; + $this->multipartBodyPartCount = 0; + $this->cursor = 0; + $this->postCount = 0; + $this->filesCount = 0; + $this->emptyCount = 0; + $this->maxFileSize = null; + + return $request; + } + + private function parseBody($boundary, $buffer) + { + $len = \strlen($boundary); + + // ignore everything before initial boundary (SHOULD be empty) + $this->cursor = \strpos($buffer, $boundary . "\r\n"); + + while ($this->cursor !== false) { + // search following boundary (preceded by newline) + // ignore last if not followed by boundary (SHOULD end with "--") + $this->cursor += $len + 2; + $end = \strpos($buffer, "\r\n" . $boundary, $this->cursor); + if ($end === false) { + break; + } + + // parse one part and continue searching for next + $this->parsePart(\substr($buffer, $this->cursor, $end - $this->cursor)); + $this->cursor = $end; + + if (++$this->multipartBodyPartCount > $this->maxMultipartBodyParts) { + break; + } + } + } + + private function parsePart($chunk) + { + $pos = \strpos($chunk, "\r\n\r\n"); + if ($pos === false) { + return; + } + + $headers = $this->parseHeaders((string)substr($chunk, 0, $pos)); + $body = (string)\substr($chunk, $pos + 4); + + if (!isset($headers['content-disposition'])) { + return; + } + + $name = $this->getParameterFromHeader($headers['content-disposition'], 'name'); + if ($name === null) { + return; + } + + $filename = $this->getParameterFromHeader($headers['content-disposition'], 'filename'); + if ($filename !== null) { + $this->parseFile( + $name, + $filename, + isset($headers['content-type'][0]) ? $headers['content-type'][0] : null, + $body + ); + } else { + $this->parsePost($name, $body); + } + } + + private function parseFile($name, $filename, $contentType, $contents) + { + $file = $this->parseUploadedFile($filename, $contentType, $contents); + if ($file === null) { + return; + } + + $this->request = $this->request->withUploadedFiles($this->extractPost( + $this->request->getUploadedFiles(), + $name, + $file + )); + } + + private function parseUploadedFile($filename, $contentType, $contents) + { + $size = \strlen($contents); + + // no file selected (zero size and empty filename) + if ($size === 0 && $filename === '') { + // ignore excessive number of empty file uploads + if (++$this->emptyCount + $this->filesCount > $this->maxInputVars) { + return; + } + + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_NO_FILE, + $filename, + $contentType + ); + } + + // ignore excessive number of file uploads + if (++$this->filesCount > $this->maxFileUploads) { + return; + } + + // file exceeds "upload_max_filesize" ini setting + if ($size > $this->uploadMaxFilesize) { + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_INI_SIZE, + $filename, + $contentType + ); + } + + // file exceeds MAX_FILE_SIZE value + if ($this->maxFileSize !== null && $size > $this->maxFileSize) { + return new UploadedFile( + new BufferedBody(''), + $size, + \UPLOAD_ERR_FORM_SIZE, + $filename, + $contentType + ); + } + + return new UploadedFile( + new BufferedBody($contents), + $size, + \UPLOAD_ERR_OK, + $filename, + $contentType + ); + } + + private function parsePost($name, $value) + { + // ignore excessive number of post fields + if (++$this->postCount > $this->maxInputVars) { + return; + } + + $this->request = $this->request->withParsedBody($this->extractPost( + $this->request->getParsedBody(), + $name, + $value + )); + + if (\strtoupper($name) === 'MAX_FILE_SIZE') { + $this->maxFileSize = (int)$value; + + if ($this->maxFileSize === 0) { + $this->maxFileSize = null; + } + } + } + + private function parseHeaders($header) + { + $headers = array(); + + foreach (\explode("\r\n", \trim($header)) as $line) { + $parts = \explode(':', $line, 2); + if (!isset($parts[1])) { + continue; + } + + $key = \strtolower(trim($parts[0])); + $values = \explode(';', $parts[1]); + $values = \array_map('trim', $values); + $headers[$key] = $values; + } + + return $headers; + } + + private function getParameterFromHeader(array $header, $parameter) + { + foreach ($header as $part) { + if (\preg_match('/' . $parameter . '="?(.*?)"?$/', $part, $matches)) { + return $matches[1]; + } + } + + return null; + } + + private function extractPost($postFields, $key, $value) + { + $chunks = \explode('[', $key); + if (\count($chunks) == 1) { + $postFields[$key] = $value; + return $postFields; + } + + // ignore this key if maximum nesting level is exceeded + if (isset($chunks[$this->maxInputNestingLevel])) { + return $postFields; + } + + $chunkKey = \rtrim($chunks[0], ']'); + $parent = &$postFields; + for ($i = 1; isset($chunks[$i]); $i++) { + $previousChunkKey = $chunkKey; + + if ($previousChunkKey === '') { + $parent[] = array(); + \end($parent); + $parent = &$parent[\key($parent)]; + } else { + if (!isset($parent[$previousChunkKey]) || !\is_array($parent[$previousChunkKey])) { + $parent[$previousChunkKey] = array(); + } + $parent = &$parent[$previousChunkKey]; + } + + $chunkKey = \rtrim($chunks[$i], ']'); + } + + if ($chunkKey === '') { + $parent[] = $value; + } else { + $parent[$chunkKey] = $value; + } + + return $postFields; + } +} diff --git a/vendor/react/http/src/Io/PauseBufferStream.php b/vendor/react/http/src/Io/PauseBufferStream.php new file mode 100644 index 0000000..fb5ed45 --- /dev/null +++ b/vendor/react/http/src/Io/PauseBufferStream.php @@ -0,0 +1,188 @@ +input = $input; + + $this->input->on('data', array($this, 'handleData')); + $this->input->on('end', array($this, 'handleEnd')); + $this->input->on('error', array($this, 'handleError')); + $this->input->on('close', array($this, 'handleClose')); + } + + /** + * pause and remember this was not explicitly from user control + * + * @internal + */ + public function pauseImplicit() + { + $this->pause(); + $this->implicit = true; + } + + /** + * resume only if this was previously paused implicitly and not explicitly from user control + * + * @internal + */ + public function resumeImplicit() + { + if ($this->implicit) { + $this->resume(); + } + } + + public function isReadable() + { + return !$this->closed; + } + + public function pause() + { + if ($this->closed) { + return; + } + + $this->input->pause(); + $this->paused = true; + $this->implicit = false; + } + + public function resume() + { + if ($this->closed) { + return; + } + + $this->paused = false; + $this->implicit = false; + + if ($this->dataPaused !== '') { + $this->emit('data', array($this->dataPaused)); + $this->dataPaused = ''; + } + + if ($this->errorPaused) { + $this->emit('error', array($this->errorPaused)); + return $this->close(); + } + + if ($this->endPaused) { + $this->endPaused = false; + $this->emit('end'); + return $this->close(); + } + + if ($this->closePaused) { + $this->closePaused = false; + return $this->close(); + } + + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + Util::pipe($this, $dest, $options); + + return $dest; + } + + public function close() + { + if ($this->closed) { + return; + } + + $this->closed = true; + $this->dataPaused = ''; + $this->endPaused = $this->closePaused = false; + $this->errorPaused = null; + + $this->input->close(); + + $this->emit('close'); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleData($data) + { + if ($this->paused) { + $this->dataPaused .= $data; + return; + } + + $this->emit('data', array($data)); + } + + /** @internal */ + public function handleError(\Exception $e) + { + if ($this->paused) { + $this->errorPaused = $e; + return; + } + + $this->emit('error', array($e)); + $this->close(); + } + + /** @internal */ + public function handleEnd() + { + if ($this->paused) { + $this->endPaused = true; + return; + } + + if (!$this->closed) { + $this->emit('end'); + $this->close(); + } + } + + /** @internal */ + public function handleClose() + { + if ($this->paused) { + $this->closePaused = true; + return; + } + + $this->close(); + } +} diff --git a/vendor/react/http/src/Io/ReadableBodyStream.php b/vendor/react/http/src/Io/ReadableBodyStream.php new file mode 100644 index 0000000..daef45f --- /dev/null +++ b/vendor/react/http/src/Io/ReadableBodyStream.php @@ -0,0 +1,153 @@ +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/react/http/src/Io/RequestHeaderParser.php b/vendor/react/http/src/Io/RequestHeaderParser.php new file mode 100644 index 0000000..8975ce5 --- /dev/null +++ b/vendor/react/http/src/Io/RequestHeaderParser.php @@ -0,0 +1,179 @@ +> */ + private $connectionParams = array(); + + public function __construct(Clock $clock) + { + $this->clock = $clock; + } + + public function handle(ConnectionInterface $conn) + { + $buffer = ''; + $maxSize = $this->maxSize; + $that = $this; + $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) { + // append chunk of data to buffer and look for end of request headers + $buffer .= $data; + $endOfHeader = \strpos($buffer, "\r\n\r\n"); + + // reject request if buffer size is exceeded + if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) { + $conn->removeListener('data', $fn); + $fn = null; + + $that->emit('error', array( + new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE), + $conn + )); + return; + } + + // ignore incomplete requests + if ($endOfHeader === false) { + return; + } + + // request headers received => try to parse request + $conn->removeListener('data', $fn); + $fn = null; + + try { + $request = $that->parseRequest( + (string)\substr($buffer, 0, $endOfHeader + 2), + $conn + ); + } catch (Exception $exception) { + $buffer = ''; + $that->emit('error', array( + $exception, + $conn + )); + return; + } + + $contentLength = 0; + if ($request->hasHeader('Transfer-Encoding')) { + $contentLength = null; + } elseif ($request->hasHeader('Content-Length')) { + $contentLength = (int)$request->getHeaderLine('Content-Length'); + } + + if ($contentLength === 0) { + // happy path: request body is known to be empty + $stream = new EmptyBodyStream(); + $request = $request->withBody($stream); + } else { + // otherwise body is present => delimit using Content-Length or ChunkedDecoder + $stream = new CloseProtectionStream($conn); + if ($contentLength !== null) { + $stream = new LengthLimitedStream($stream, $contentLength); + } else { + $stream = new ChunkedDecoder($stream); + } + + $request = $request->withBody(new HttpBodyStream($stream, $contentLength)); + } + + $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : ''; + $buffer = ''; + $that->emit('headers', array($request, $conn)); + + if ($bodyBuffer !== '') { + $conn->emit('data', array($bodyBuffer)); + } + + // happy path: request body is known to be empty => immediately end stream + if ($contentLength === 0) { + $stream->emit('end'); + $stream->close(); + } + }); + } + + /** + * @param string $headers buffer string containing request headers only + * @param ConnectionInterface $connection + * @return ServerRequestInterface + * @throws \InvalidArgumentException + * @internal + */ + public function parseRequest($headers, ConnectionInterface $connection) + { + // reuse same connection params for all server params for this connection + $cid = \PHP_VERSION_ID < 70200 ? \spl_object_hash($connection) : \spl_object_id($connection); + if (isset($this->connectionParams[$cid])) { + $serverParams = $this->connectionParams[$cid]; + } else { + // assign new server params for new connection + $serverParams = array(); + + // scheme is `http` unless TLS is used + $localSocketUri = $connection->getLocalAddress(); + $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri); + if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') { + $serverParams['HTTPS'] = 'on'; + } + + // apply SERVER_ADDR and SERVER_PORT if server address is known + // address should always be known, even for Unix domain sockets (UDS) + // but skip UDS as it doesn't have a concept of host/port. + if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) { + $serverParams['SERVER_ADDR'] = $localParts['host']; + $serverParams['SERVER_PORT'] = $localParts['port']; + } + + // apply REMOTE_ADDR and REMOTE_PORT if source address is known + // address should always be known, unless this is over Unix domain sockets (UDS) + $remoteSocketUri = $connection->getRemoteAddress(); + if ($remoteSocketUri !== null) { + $remoteAddress = \parse_url($remoteSocketUri); + $serverParams['REMOTE_ADDR'] = $remoteAddress['host']; + $serverParams['REMOTE_PORT'] = $remoteAddress['port']; + } + + // remember server params for all requests from this connection, reset on connection close + $this->connectionParams[$cid] = $serverParams; + $params =& $this->connectionParams; + $connection->on('close', function () use (&$params, $cid) { + assert(\is_array($params)); + unset($params[$cid]); + }); + } + + // create new obj implementing ServerRequestInterface by preserving all + // previous properties and restoring original request-target + $serverParams['REQUEST_TIME'] = (int) ($now = $this->clock->now()); + $serverParams['REQUEST_TIME_FLOAT'] = $now; + + return ServerRequest::parseMessage($headers, $serverParams); + } +} diff --git a/vendor/react/http/src/Io/Sender.php b/vendor/react/http/src/Io/Sender.php new file mode 100644 index 0000000..5f456b2 --- /dev/null +++ b/vendor/react/http/src/Io/Sender.php @@ -0,0 +1,152 @@ +http = $http; + } + + /** + * + * @internal + * @param RequestInterface $request + * @return PromiseInterface Promise + */ + public function send(RequestInterface $request) + { + // support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already + assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true)); + + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size !== 0) { + // automatically assign a "Content-Length" request header if the body size is known and non-empty + $request = $request->withHeader('Content-Length', (string)$size); + } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { + // only assign a "Content-Length: 0" request header if the body is expected for certain methods + $request = $request->withHeader('Content-Length', '0'); + } elseif ($body instanceof ReadableStreamInterface && $size !== 0 && $body->isReadable() && !$request->hasHeader('Content-Length')) { + // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown + $request = $request->withHeader('Transfer-Encoding', 'chunked'); + } else { + // do not use chunked encoding if size is known or if this is an empty request body + $size = 0; + } + + // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` + if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) { + $request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo())); + } + + $requestStream = $this->http->request($request); + + $deferred = new Deferred(function ($_, $reject) use ($requestStream) { + // close request stream if request is cancelled + $reject(new \RuntimeException('Request cancelled')); + $requestStream->close(); + }); + + $requestStream->on('error', function($error) use ($deferred) { + $deferred->reject($error); + }); + + $requestStream->on('response', function (ResponseInterface $response) use ($deferred, $request) { + $deferred->resolve($response); + }); + + if ($body instanceof ReadableStreamInterface) { + if ($body->isReadable()) { + // length unknown => apply chunked transfer-encoding + if ($size === null) { + $body = new ChunkedEncoder($body); + } + + // pipe body into request stream + // add dummy write to immediately start request even if body does not emit any data yet + $body->pipe($requestStream); + $requestStream->write(''); + + $body->on('close', $close = function () use ($deferred, $requestStream) { + $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); + $requestStream->close(); + }); + $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { + $body->removeListener('close', $close); + $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); + $requestStream->close(); + }); + $body->on('end', function () use ($close, $body) { + $body->removeListener('close', $close); + }); + } else { + // stream is not readable => end request without body + $requestStream->end(); + } + } else { + // body is fully buffered => write as one chunk + $requestStream->end((string)$body); + } + + return $deferred->promise(); + } +} diff --git a/vendor/react/http/src/Io/StreamingServer.php b/vendor/react/http/src/Io/StreamingServer.php new file mode 100644 index 0000000..143edaa --- /dev/null +++ b/vendor/react/http/src/Io/StreamingServer.php @@ -0,0 +1,405 @@ + 'text/plain' + * ), + * "Hello World!\n" + * ); + * }); + * ``` + * + * Each incoming HTTP request message is always represented by the + * [PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface), + * see also following [request](#request) chapter for more details. + * Each outgoing HTTP response message is always represented by the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface), + * see also following [response](#response) chapter for more details. + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method + * as described in the following chapter. In its most simple form, you can attach + * this to a [`React\Socket\SocketServer`](https://github.com/reactphp/socket#socketserver) + * in order to start a plaintext HTTP server like this: + * + * ```php + * $server = new StreamingServer($loop, $handler); + * + * $socket = new React\Socket\SocketServer('0.0.0.0:8080', array(), $loop); + * $server->listen($socket); + * ``` + * + * See also the [`listen()`](#listen) method and the [first example](examples) for more details. + * + * The `StreamingServer` class is considered advanced usage and unless you know + * what you're doing, you're recommended to use the [`HttpServer`](#httpserver) class + * instead. The `StreamingServer` class is specifically designed to help with + * more advanced use cases where you want to have full control over consuming + * the incoming HTTP request body and concurrency settings. + * + * In particular, this class does not buffer and parse the incoming HTTP request + * in memory. It will invoke the request handler function once the HTTP request + * headers have been received, i.e. before receiving the potentially much larger + * HTTP request body. This means the [request](#request) passed to your request + * handler function may not be fully compatible with PSR-7. See also + * [streaming request](#streaming-request) below for more details. + * + * @see \React\Http\HttpServer + * @see \React\Http\Message\Response + * @see self::listen() + * @internal + */ +final class StreamingServer extends EventEmitter +{ + private $callback; + private $parser; + + /** @var Clock */ + private $clock; + + /** + * Creates an HTTP server that invokes the given callback for each incoming HTTP request + * + * In order to process any connections, the server needs to be attached to an + * instance of `React\Socket\ServerInterface` which emits underlying streaming + * connections in order to then parse incoming data as HTTP. + * See also [listen()](#listen) for more details. + * + * @param LoopInterface $loop + * @param callable $requestHandler + * @see self::listen() + */ + public function __construct(LoopInterface $loop, $requestHandler) + { + if (!\is_callable($requestHandler)) { + throw new \InvalidArgumentException('Invalid request handler given'); + } + + $this->callback = $requestHandler; + $this->clock = new Clock($loop); + $this->parser = new RequestHeaderParser($this->clock); + + $that = $this; + $this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) { + $that->handleRequest($conn, $request); + }); + + $this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) { + $that->emit('error', array($e)); + + // parsing failed => assume dummy request and send appropriate error + $that->writeError( + $conn, + $e->getCode() !== 0 ? $e->getCode() : Response::STATUS_BAD_REQUEST, + new ServerRequest('GET', '/') + ); + }); + } + + /** + * Starts listening for HTTP requests on the given socket server instance + * + * @param ServerInterface $socket + * @see \React\Http\HttpServer::listen() + */ + public function listen(ServerInterface $socket) + { + $socket->on('connection', array($this->parser, 'handle')); + } + + /** @internal */ + public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request) + { + if ($request->getProtocolVersion() !== '1.0' && '100-continue' === \strtolower($request->getHeaderLine('Expect'))) { + $conn->write("HTTP/1.1 100 Continue\r\n\r\n"); + } + + // execute request handler callback + $callback = $this->callback; + try { + $response = $callback($request); + } catch (\Exception $error) { + // request handler callback throws an Exception + $response = Promise\reject($error); + } catch (\Throwable $error) { // @codeCoverageIgnoreStart + // request handler callback throws a PHP7+ Error + $response = Promise\reject($error); // @codeCoverageIgnoreEnd + } + + // cancel pending promise once connection closes + $connectionOnCloseResponseCancelerHandler = function () {}; + if ($response instanceof PromiseInterface && \method_exists($response, 'cancel')) { + $connectionOnCloseResponseCanceler = function () use ($response) { + $response->cancel(); + }; + $connectionOnCloseResponseCancelerHandler = function () use ($connectionOnCloseResponseCanceler, $conn) { + if ($connectionOnCloseResponseCanceler !== null) { + $conn->removeListener('close', $connectionOnCloseResponseCanceler); + } + }; + $conn->on('close', $connectionOnCloseResponseCanceler); + } + + // happy path: response returned, handle and return immediately + if ($response instanceof ResponseInterface) { + return $this->handleResponse($conn, $request, $response); + } + + // did not return a promise? this is an error, convert into one for rejection below. + if (!$response instanceof PromiseInterface) { + $response = Promise\resolve($response); + } + + $that = $this; + $response->then( + function ($response) use ($that, $conn, $request) { + if (!$response instanceof ResponseInterface) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but resolved with "%s" instead.'; + $message = \sprintf($message, \is_object($response) ? \get_class($response) : \gettype($response)); + $exception = new \RuntimeException($message); + + $that->emit('error', array($exception)); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + } + $that->handleResponse($conn, $request, $response); + }, + function ($error) use ($that, $conn, $request) { + $message = 'The response callback is expected to resolve with an object implementing Psr\Http\Message\ResponseInterface, but rejected with "%s" instead.'; + $message = \sprintf($message, \is_object($error) ? \get_class($error) : \gettype($error)); + + $previous = null; + + if ($error instanceof \Throwable || $error instanceof \Exception) { + $previous = $error; + } + + $exception = new \RuntimeException($message, 0, $previous); + + $that->emit('error', array($exception)); + return $that->writeError($conn, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + } + )->then($connectionOnCloseResponseCancelerHandler, $connectionOnCloseResponseCancelerHandler); + } + + /** @internal */ + public function writeError(ConnectionInterface $conn, $code, ServerRequestInterface $request) + { + $response = new Response( + $code, + array( + 'Content-Type' => 'text/plain', + 'Connection' => 'close' // we do not want to keep the connection open after an error + ), + 'Error ' . $code + ); + + // append reason phrase to response body if known + $reason = $response->getReasonPhrase(); + if ($reason !== '') { + $body = $response->getBody(); + $body->seek(0, SEEK_END); + $body->write(': ' . $reason); + } + + $this->handleResponse($conn, $request, $response); + } + + + /** @internal */ + public function handleResponse(ConnectionInterface $connection, ServerRequestInterface $request, ResponseInterface $response) + { + // return early and close response body if connection is already closed + $body = $response->getBody(); + if (!$connection->isWritable()) { + $body->close(); + return; + } + + $code = $response->getStatusCode(); + $method = $request->getMethod(); + + // assign HTTP protocol version from request automatically + $version = $request->getProtocolVersion(); + $response = $response->withProtocolVersion($version); + + // assign default "Server" header automatically + if (!$response->hasHeader('Server')) { + $response = $response->withHeader('Server', 'ReactPHP/1'); + } elseif ($response->getHeaderLine('Server') === ''){ + $response = $response->withoutHeader('Server'); + } + + // assign default "Date" header from current time automatically + if (!$response->hasHeader('Date')) { + // IMF-fixdate = day-name "," SP date1 SP time-of-day SP GMT + $response = $response->withHeader('Date', gmdate('D, d M Y H:i:s', (int) $this->clock->now()) . ' GMT'); + } elseif ($response->getHeaderLine('Date') === ''){ + $response = $response->withoutHeader('Date'); + } + + // assign "Content-Length" header automatically + $chunked = false; + if (($method === 'CONNECT' && $code >= 200 && $code < 300) || ($code >= 100 && $code < 200) || $code === Response::STATUS_NO_CONTENT) { + // 2xx response to CONNECT and 1xx and 204 MUST NOT include Content-Length or Transfer-Encoding header + $response = $response->withoutHeader('Content-Length'); + } elseif ($method === 'HEAD' && $response->hasHeader('Content-Length')) { + // HEAD Request: preserve explicit Content-Length + } elseif ($code === Response::STATUS_NOT_MODIFIED && ($response->hasHeader('Content-Length') || $body->getSize() === 0)) { + // 304 Not Modified: preserve explicit Content-Length and preserve missing header if body is empty + } elseif ($body->getSize() !== null) { + // assign Content-Length header when using a "normal" buffered body string + $response = $response->withHeader('Content-Length', (string)$body->getSize()); + } elseif (!$response->hasHeader('Content-Length') && $version === '1.1') { + // assign chunked transfer-encoding if no 'content-length' is given for HTTP/1.1 responses + $chunked = true; + } + + // assign "Transfer-Encoding" header automatically + if ($chunked) { + $response = $response->withHeader('Transfer-Encoding', 'chunked'); + } else { + // remove any Transfer-Encoding headers unless automatically enabled above + $response = $response->withoutHeader('Transfer-Encoding'); + } + + // assign "Connection" header automatically + $persist = false; + if ($code === Response::STATUS_SWITCHING_PROTOCOLS) { + // 101 (Switching Protocols) response uses Connection: upgrade header + // This implies that this stream now uses another protocol and we + // may not persist this connection for additional requests. + $response = $response->withHeader('Connection', 'upgrade'); + } elseif (\strtolower($request->getHeaderLine('Connection')) === 'close' || \strtolower($response->getHeaderLine('Connection')) === 'close') { + // obey explicit "Connection: close" request header or response header if present + $response = $response->withHeader('Connection', 'close'); + } elseif ($version === '1.1') { + // HTTP/1.1 assumes persistent connection support by default, so we don't need to inform client + $persist = true; + } elseif (strtolower($request->getHeaderLine('Connection')) === 'keep-alive') { + // obey explicit "Connection: keep-alive" request header and inform client + $persist = true; + $response = $response->withHeader('Connection', 'keep-alive'); + } else { + // remove any Connection headers unless automatically enabled above + $response = $response->withoutHeader('Connection'); + } + + // 101 (Switching Protocols) response (for Upgrade request) forwards upgraded data through duplex stream + // 2xx (Successful) response to CONNECT forwards tunneled application data through duplex stream + if (($code === Response::STATUS_SWITCHING_PROTOCOLS || ($method === 'CONNECT' && $code >= 200 && $code < 300)) && $body instanceof HttpBodyStream && $body->input instanceof WritableStreamInterface) { + if ($request->getBody()->isReadable()) { + // request is still streaming => wait for request close before forwarding following data from connection + $request->getBody()->on('close', function () use ($connection, $body) { + if ($body->input->isWritable()) { + $connection->pipe($body->input); + $connection->resume(); + } + }); + } elseif ($body->input->isWritable()) { + // request already closed => forward following data from connection + $connection->pipe($body->input); + $connection->resume(); + } + } + + // build HTTP response header by appending status line and header fields + $expected = 0; + $headers = "HTTP/" . $version . " " . $code . " " . $response->getReasonPhrase() . "\r\n"; + foreach ($response->getHeaders() as $name => $values) { + if (\strpos($name, ':') !== false) { + $expected = -1; + break; + } + foreach ($values as $value) { + $headers .= $name . ": " . $value . "\r\n"; + ++$expected; + } + } + + /** @var array $m legacy PHP 5.3 only */ + if ($code < 100 || $code > 999 || \substr_count($headers, "\n") !== ($expected + 1) || (\PHP_VERSION_ID >= 50400 ? \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers) : \preg_match_all(AbstractMessage::REGEX_HEADERS, $headers, $m)) !== $expected) { + $this->emit('error', array(new \InvalidArgumentException('Unable to send response with invalid response headers'))); + $this->writeError($connection, Response::STATUS_INTERNAL_SERVER_ERROR, $request); + return; + } + + // response to HEAD and 1xx, 204 and 304 responses MUST NOT include a body + // exclude status 101 (Switching Protocols) here for Upgrade request handling above + if ($method === 'HEAD' || ($code >= 100 && $code < 200 && $code !== Response::STATUS_SWITCHING_PROTOCOLS) || $code === Response::STATUS_NO_CONTENT || $code === Response::STATUS_NOT_MODIFIED) { + $body->close(); + $body = ''; + } + + // this is a non-streaming response body or the body stream already closed? + if (!$body instanceof ReadableStreamInterface || !$body->isReadable()) { + // add final chunk if a streaming body is already closed and uses `Transfer-Encoding: chunked` + if ($body instanceof ReadableStreamInterface && $chunked) { + $body = "0\r\n\r\n"; + } + + // write response headers and body + $connection->write($headers . "\r\n" . $body); + + // either wait for next request over persistent connection or end connection + if ($persist) { + $this->parser->handle($connection); + } else { + $connection->end(); + } + return; + } + + $connection->write($headers . "\r\n"); + + if ($chunked) { + $body = new ChunkedEncoder($body); + } + + // Close response stream once connection closes. + // Note that this TCP/IP close detection may take some time, + // in particular this may only fire on a later read/write attempt. + $connection->on('close', array($body, 'close')); + + // write streaming body and then wait for next request over persistent connection + if ($persist) { + $body->pipe($connection, array('end' => false)); + $parser = $this->parser; + $body->on('end', function () use ($connection, $parser, $body) { + $connection->removeListener('close', array($body, 'close')); + $parser->handle($connection); + }); + } else { + $body->pipe($connection); + } + } +} diff --git a/vendor/react/http/src/Io/Transaction.php b/vendor/react/http/src/Io/Transaction.php new file mode 100644 index 0000000..64738f5 --- /dev/null +++ b/vendor/react/http/src/Io/Transaction.php @@ -0,0 +1,330 @@ +sender = $sender; + $this->loop = $loop; + } + + /** + * @param array $options + * @return self returns new instance, without modifying existing instance + */ + public function withOptions(array $options) + { + $transaction = clone $this; + foreach ($options as $name => $value) { + if (property_exists($transaction, $name)) { + // restore default value if null is given + if ($value === null) { + $default = new self($this->sender, $this->loop); + $value = $default->$name; + } + + $transaction->$name = $value; + } + } + + return $transaction; + } + + public function send(RequestInterface $request) + { + $state = new ClientRequestState(); + $deferred = new Deferred(function () use ($state) { + if ($state->pending !== null) { + $state->pending->cancel(); + $state->pending = null; + } + }); + + // use timeout from options or default to PHP's default_socket_timeout (60) + $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); + + $loop = $this->loop; + $this->next($request, $deferred, $state)->then( + function (ResponseInterface $response) use ($state, $deferred, $loop, &$timeout) { + if ($state->timeout !== null) { + $loop->cancelTimer($state->timeout); + $state->timeout = null; + } + $timeout = -1; + $deferred->resolve($response); + }, + function ($e) use ($state, $deferred, $loop, &$timeout) { + if ($state->timeout !== null) { + $loop->cancelTimer($state->timeout); + $state->timeout = null; + } + $timeout = -1; + $deferred->reject($e); + } + ); + + if ($timeout < 0) { + return $deferred->promise(); + } + + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface && $body->isReadable()) { + $that = $this; + $body->on('close', function () use ($that, $deferred, $state, &$timeout) { + if ($timeout >= 0) { + $that->applyTimeout($deferred, $state, $timeout); + } + }); + } else { + $this->applyTimeout($deferred, $state, $timeout); + } + + return $deferred->promise(); + } + + /** + * @internal + * @param number $timeout + * @return void + */ + public function applyTimeout(Deferred $deferred, ClientRequestState $state, $timeout) + { + $state->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred, $state) { + $deferred->reject(new \RuntimeException( + 'Request timed out after ' . $timeout . ' seconds' + )); + if ($state->pending !== null) { + $state->pending->cancel(); + $state->pending = null; + } + }); + } + + private function next(RequestInterface $request, Deferred $deferred, ClientRequestState $state) + { + $this->progress('request', array($request)); + + $that = $this; + ++$state->numRequests; + + $promise = $this->sender->send($request); + + if (!$this->streaming) { + $promise = $promise->then(function ($response) use ($deferred, $state, $that) { + return $that->bufferResponse($response, $deferred, $state); + }); + } + + $state->pending = $promise; + + return $promise->then( + function (ResponseInterface $response) use ($request, $that, $deferred, $state) { + return $that->onResponse($response, $request, $deferred, $state); + } + ); + } + + /** + * @internal + * @return PromiseInterface Promise + */ + public function bufferResponse(ResponseInterface $response, Deferred $deferred, ClientRequestState $state) + { + $body = $response->getBody(); + $size = $body->getSize(); + + if ($size !== null && $size > $this->maximumSize) { + $body->close(); + return \React\Promise\reject(new \OverflowException( + 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 + )); + } + + // body is not streaming => already buffered + if (!$body instanceof ReadableStreamInterface) { + return \React\Promise\resolve($response); + } + + /** @var ?\Closure $closer */ + $closer = null; + $maximumSize = $this->maximumSize; + + return $state->pending = new Promise(function ($resolve, $reject) use ($body, $maximumSize, $response, &$closer) { + // resolve with current buffer when stream closes successfully + $buffer = ''; + $body->on('close', $closer = function () use (&$buffer, $response, $maximumSize, $resolve, $reject) { + $resolve($response->withBody(new BufferedBody($buffer))); + }); + + // buffer response body data in memory + $body->on('data', function ($data) use (&$buffer, $maximumSize, $body, $closer, $reject) { + $buffer .= $data; + + // close stream and reject promise if limit is exceeded + if (isset($buffer[$maximumSize])) { + $buffer = ''; + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); + + $reject(new \OverflowException( + 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', + \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 + )); + } + }); + + // reject buffering if body emits error + $body->on('error', function (\Exception $e) use ($reject) { + $reject(new \RuntimeException( + 'Error while buffering response body: ' . $e->getMessage(), + $e->getCode(), + $e + )); + }); + }, function () use ($body, &$closer) { + // cancelled buffering: remove close handler to avoid resolving, then close and reject + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); + + throw new \RuntimeException('Cancelled buffering response body'); + }); + } + + /** + * @internal + * @throws ResponseException + * @return ResponseInterface|PromiseInterface + */ + public function onResponse(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) + { + $this->progress('response', array($response, $request)); + + // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled + // @link https://tools.ietf.org/html/rfc7231#section-6.4 + if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { + return $this->onResponseRedirect($response, $request, $deferred, $state); + } + + // only status codes 200-399 are considered to be valid, reject otherwise + if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { + throw new ResponseException($response); + } + + // resolve our initial promise + return $response; + } + + /** + * @param ResponseInterface $response + * @param RequestInterface $request + * @param Deferred $deferred + * @param ClientRequestState $state + * @return PromiseInterface + * @throws \RuntimeException + */ + private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred, ClientRequestState $state) + { + // resolve location relative to last request URI + $location = Uri::resolve($request->getUri(), new Uri($response->getHeaderLine('Location'))); + + $request = $this->makeRedirectRequest($request, $location, $response->getStatusCode()); + $this->progress('redirect', array($request)); + + if ($state->numRequests >= $this->maxRedirects) { + throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); + } + + return $this->next($request, $deferred, $state); + } + + /** + * @param RequestInterface $request + * @param UriInterface $location + * @param int $statusCode + * @return RequestInterface + * @throws \RuntimeException + */ + private function makeRedirectRequest(RequestInterface $request, UriInterface $location, $statusCode) + { + // Remove authorization if changing hostnames (but not if just changing ports or protocols). + $originalHost = $request->getUri()->getHost(); + if ($location->getHost() !== $originalHost) { + $request = $request->withoutHeader('Authorization'); + } + + $request = $request->withoutHeader('Host')->withUri($location); + + if ($statusCode === Response::STATUS_TEMPORARY_REDIRECT || $statusCode === Response::STATUS_PERMANENT_REDIRECT) { + if ($request->getBody() instanceof ReadableStreamInterface) { + throw new \RuntimeException('Unable to redirect request with streaming body'); + } + } else { + $request = $request + ->withMethod($request->getMethod() === 'HEAD' ? 'HEAD' : 'GET') + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length') + ->withBody(new BufferedBody('')); + } + + return $request; + } + + private function progress($name, array $args = array()) + { + return; + + echo $name; + + foreach ($args as $arg) { + echo ' '; + if ($arg instanceof ResponseInterface) { + echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); + } elseif ($arg instanceof RequestInterface) { + echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); + } else { + echo $arg; + } + } + + echo PHP_EOL; + } +} diff --git a/vendor/react/http/src/Io/UploadedFile.php b/vendor/react/http/src/Io/UploadedFile.php new file mode 100644 index 0000000..f2a6c9e --- /dev/null +++ b/vendor/react/http/src/Io/UploadedFile.php @@ -0,0 +1,130 @@ +stream = $stream; + $this->size = $size; + + if (!\is_int($error) || !\in_array($error, array( + \UPLOAD_ERR_OK, + \UPLOAD_ERR_INI_SIZE, + \UPLOAD_ERR_FORM_SIZE, + \UPLOAD_ERR_PARTIAL, + \UPLOAD_ERR_NO_FILE, + \UPLOAD_ERR_NO_TMP_DIR, + \UPLOAD_ERR_CANT_WRITE, + \UPLOAD_ERR_EXTENSION, + ))) { + throw new InvalidArgumentException( + 'Invalid error code, must be an UPLOAD_ERR_* constant' + ); + } + $this->error = $error; + $this->filename = $filename; + $this->mediaType = $mediaType; + } + + /** + * {@inheritdoc} + */ + public function getStream() + { + if ($this->error !== \UPLOAD_ERR_OK) { + throw new RuntimeException('Cannot retrieve stream due to upload error'); + } + + return $this->stream; + } + + /** + * {@inheritdoc} + */ + public function moveTo($targetPath) + { + throw new RuntimeException('Not implemented'); + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return $this->size; + } + + /** + * {@inheritdoc} + */ + public function getError() + { + return $this->error; + } + + /** + * {@inheritdoc} + */ + public function getClientFilename() + { + return $this->filename; + } + + /** + * {@inheritdoc} + */ + public function getClientMediaType() + { + return $this->mediaType; + } +} diff --git a/vendor/react/http/src/Message/Request.php b/vendor/react/http/src/Message/Request.php new file mode 100644 index 0000000..3de8c1b --- /dev/null +++ b/vendor/react/http/src/Message/Request.php @@ -0,0 +1,57 @@ + Internally, this implementation builds on top of a base class which is + * considered an implementation detail that may change in the future. + * + * @see RequestInterface + */ +final class Request extends AbstractRequest implements RequestInterface +{ + /** + * @param string $method HTTP method for the request. + * @param string|UriInterface $url URL for the request. + * @param array $headers Headers for the message. + * @param string|ReadableStreamInterface|StreamInterface $body Message body. + * @param string $version HTTP protocol version. + * @throws \InvalidArgumentException for an invalid URL or body + */ + public function __construct( + $method, + $url, + array $headers = array(), + $body = '', + $version = '1.1' + ) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $body = new ReadableBodyStream($body); + } elseif (!$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid request body given'); + } + + parent::__construct($method, $url, $headers, $body, $version); + } +} diff --git a/vendor/react/http/src/Message/Response.php b/vendor/react/http/src/Message/Response.php new file mode 100644 index 0000000..fa6366e --- /dev/null +++ b/vendor/react/http/src/Message/Response.php @@ -0,0 +1,414 @@ + 'text/html' + * ), + * "Hello world!\n" + * ); + * ``` + * + * This class implements the + * [PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface) + * which in turn extends the + * [PSR-7 `MessageInterface`](https://www.php-fig.org/psr/psr-7/#31-psrhttpmessagemessageinterface). + * + * On top of this, this class implements the + * [PSR-7 Message Util `StatusCodeInterface`](https://github.com/php-fig/http-message-util/blob/master/src/StatusCodeInterface.php) + * which means that most common HTTP status codes are available as class + * constants with the `STATUS_*` prefix. For instance, the `200 OK` and + * `404 Not Found` status codes can used as `Response::STATUS_OK` and + * `Response::STATUS_NOT_FOUND` respectively. + * + * > Internally, this implementation builds on top a base class which is + * considered an implementation detail that may change in the future. + * + * @see \Psr\Http\Message\ResponseInterface + */ +final class Response extends AbstractMessage implements ResponseInterface, StatusCodeInterface +{ + /** + * Create an HTML response + * + * ```php + * $html = << + * + * Hello wörld! + * + * + * HTML; + * + * $response = React\Http\Message\Response::html($html); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/html; charset=utf-8' + * ], + * $html + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given HTTP source + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::html( + * "

Error

\n

Invalid user name given.

\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $html + * @return self + */ + public static function html($html) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/html; charset=utf-8'), $html); + } + + /** + * Create a JSON response + * + * ```php + * $response = React\Http\Message\Response::json(['name' => 'Alice']); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/json' + * ], + * json_encode( + * ['name' => 'Alice'], + * JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRESERVE_ZERO_FRACTION + * ) . "\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given structured + * data encoded as a JSON text. + * + * The given structured data will be encoded as a JSON text. Any `string` + * values in the data must be encoded in UTF-8 (Unicode). If the encoding + * fails, this method will throw an `InvalidArgumentException`. + * + * By default, the given structured data will be encoded with the flags as + * shown above. This includes pretty printing (PHP 5.4+) and preserving + * zero fractions for `float` values (PHP 5.6.6+) to ease debugging. It is + * assumed any additional data overhead is usually compensated by using HTTP + * response compression. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::json( + * ['error' => 'Invalid user name given'] + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param mixed $data + * @return self + * @throws \InvalidArgumentException when encoding fails + */ + public static function json($data) + { + $json = @\json_encode( + $data, + (\defined('JSON_PRETTY_PRINT') ? \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE : 0) | (\defined('JSON_PRESERVE_ZERO_FRACTION') ? \JSON_PRESERVE_ZERO_FRACTION : 0) + ); + + // throw on error, now `false` but used to be `(string) "null"` before PHP 5.5 + if ($json === false || (\PHP_VERSION_ID < 50500 && \json_last_error() !== \JSON_ERROR_NONE)) { + throw new \InvalidArgumentException( + 'Unable to encode given data as JSON' . (\function_exists('json_last_error_msg') ? ': ' . \json_last_error_msg() : ''), + \json_last_error() + ); + } + + return new self(self::STATUS_OK, array('Content-Type' => 'application/json'), $json . "\n"); + } + + /** + * Create a plaintext response + * + * ```php + * $response = React\Http\Message\Response::plaintext("Hello wörld!\n"); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'text/plain; charset=utf-8' + * ], + * "Hello wörld!\n" + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given plaintext + * string encoded in UTF-8 (Unicode). It's generally recommended to end the + * given plaintext string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::plaintext( + * "Error: Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $text + * @return self + */ + public static function plaintext($text) + { + return new self(self::STATUS_OK, array('Content-Type' => 'text/plain; charset=utf-8'), $text); + } + + /** + * Create an XML response + * + * ```php + * $xml = << + * + * Hello wörld! + * + * + * XML; + * + * $response = React\Http\Message\Response::xml($xml); + * ``` + * + * This is a convenient shortcut method that returns the equivalent of this: + * + * ``` + * $response = new React\Http\Message\Response( + * React\Http\Message\Response::STATUS_OK, + * [ + * 'Content-Type' => 'application/xml' + * ], + * $xml + * ); + * ``` + * + * This method always returns a response with a `200 OK` status code and + * the appropriate `Content-Type` response header for the given XML source + * string. It's generally recommended to use UTF-8 (Unicode) and specify + * this as part of the leading XML declaration and to end the given XML + * source string with a trailing newline. + * + * If you want to use a different status code or custom HTTP response + * headers, you can manipulate the returned response object using the + * provided PSR-7 methods or directly instantiate a custom HTTP response + * object using the `Response` constructor: + * + * ```php + * $response = React\Http\Message\Response::xml( + * "Invalid user name given.\n" + * )->withStatus(React\Http\Message\Response::STATUS_BAD_REQUEST); + * ``` + * + * @param string $xml + * @return self + */ + public static function xml($xml) + { + return new self(self::STATUS_OK, array('Content-Type' => 'application/xml'), $xml); + } + + /** + * @var bool + * @see self::$phrasesMap + */ + private static $phrasesInitialized = false; + + /** + * Map of standard HTTP status codes to standard reason phrases. + * + * This map will be fully populated with all standard reason phrases on + * first access. By default, it only contains a subset of HTTP status codes + * that have a custom mapping to reason phrases (such as those with dashes + * and all caps words). See `self::STATUS_*` for all possible status code + * constants. + * + * @var array + * @see self::STATUS_* + * @see self::getReasonPhraseForStatusCode() + */ + private static $phrasesMap = array( + 200 => 'OK', + 203 => 'Non-Authoritative Information', + 207 => 'Multi-Status', + 226 => 'IM Used', + 414 => 'URI Too Large', + 418 => 'I\'m a teapot', + 505 => 'HTTP Version Not Supported' + ); + + /** @var int */ + private $statusCode; + + /** @var string */ + private $reasonPhrase; + + /** + * @param int $status HTTP status code (e.g. 200/404), see `self::STATUS_*` constants + * @param array $headers additional response headers + * @param string|ReadableStreamInterface|StreamInterface $body response body + * @param string $version HTTP protocol version (e.g. 1.1/1.0) + * @param ?string $reason custom HTTP response phrase + * @throws \InvalidArgumentException for an invalid body + */ + public function __construct( + $status = self::STATUS_OK, + array $headers = array(), + $body = '', + $version = '1.1', + $reason = null + ) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $body = new HttpBodyStream($body, null); + } elseif (!$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid response body given'); + } + + parent::__construct($version, $headers, $body); + + $this->statusCode = (int) $status; + $this->reasonPhrase = ($reason !== '' && $reason !== null) ? (string) $reason : self::getReasonPhraseForStatusCode($status); + } + + public function getStatusCode() + { + return $this->statusCode; + } + + public function withStatus($code, $reasonPhrase = '') + { + if ((string) $reasonPhrase === '') { + $reasonPhrase = self::getReasonPhraseForStatusCode($code); + } + + if ($this->statusCode === (int) $code && $this->reasonPhrase === (string) $reasonPhrase) { + return $this; + } + + $response = clone $this; + $response->statusCode = (int) $code; + $response->reasonPhrase = (string) $reasonPhrase; + + return $response; + } + + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + /** + * @param int $code + * @return string default reason phrase for given status code or empty string if unknown + */ + private static function getReasonPhraseForStatusCode($code) + { + if (!self::$phrasesInitialized) { + self::$phrasesInitialized = true; + + // map all `self::STATUS_` constants from status code to reason phrase + // e.g. `self::STATUS_NOT_FOUND = 404` will be mapped to `404 Not Found` + $ref = new \ReflectionClass(__CLASS__); + foreach ($ref->getConstants() as $name => $value) { + if (!isset(self::$phrasesMap[$value]) && \strpos($name, 'STATUS_') === 0) { + self::$phrasesMap[$value] = \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 7)))); + } + } + } + + return isset(self::$phrasesMap[$code]) ? self::$phrasesMap[$code] : ''; + } + + /** + * [Internal] Parse incoming HTTP protocol message + * + * @internal + * @param string $message + * @return self + * @throws \InvalidArgumentException if given $message is not a valid HTTP response message + */ + public static function parseMessage($message) + { + $start = array(); + if (!\preg_match('#^HTTP/(?\d\.\d) (?\d{3})(?: (?[^\r\n]*+))?[\r]?+\n#m', $message, $start)) { + throw new \InvalidArgumentException('Unable to parse invalid status-line'); + } + + // only support HTTP/1.1 and HTTP/1.0 requests + if ($start['version'] !== '1.1' && $start['version'] !== '1.0') { + throw new \InvalidArgumentException('Received response with invalid protocol version'); + } + + // check number of valid header fields matches number of lines + status line + $matches = array(); + $n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER); + if (\substr_count($message, "\n") !== $n + 1) { + throw new \InvalidArgumentException('Unable to parse invalid response header fields'); + } + + // format all header fields into associative array + $headers = array(); + foreach ($matches as $match) { + $headers[$match[1]][] = $match[2]; + } + + return new self( + (int) $start['status'], + $headers, + '', + $start['version'], + isset($start['reason']) ? $start['reason'] : '' + ); + } +} diff --git a/vendor/react/http/src/Message/ResponseException.php b/vendor/react/http/src/Message/ResponseException.php new file mode 100644 index 0000000..f4912c9 --- /dev/null +++ b/vendor/react/http/src/Message/ResponseException.php @@ -0,0 +1,43 @@ +getStatusCode() . ' (' . $response->getReasonPhrase() . ')'; + } + if ($code === null) { + $code = $response->getStatusCode(); + } + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + /** + * Access its underlying response object. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/vendor/react/http/src/Message/ServerRequest.php b/vendor/react/http/src/Message/ServerRequest.php new file mode 100644 index 0000000..32a0f62 --- /dev/null +++ b/vendor/react/http/src/Message/ServerRequest.php @@ -0,0 +1,331 @@ + Internally, this implementation builds on top of a base class which is + * considered an implementation detail that may change in the future. + * + * @see ServerRequestInterface + */ +final class ServerRequest extends AbstractRequest implements ServerRequestInterface +{ + private $attributes = array(); + + private $serverParams; + private $fileParams = array(); + private $cookies = array(); + private $queryParams = array(); + private $parsedBody; + + /** + * @param string $method HTTP method for the request. + * @param string|UriInterface $url URL for the request. + * @param array $headers Headers for the message. + * @param string|ReadableStreamInterface|StreamInterface $body Message body. + * @param string $version HTTP protocol version. + * @param array $serverParams server-side parameters + * @throws \InvalidArgumentException for an invalid URL or body + */ + public function __construct( + $method, + $url, + array $headers = array(), + $body = '', + $version = '1.1', + $serverParams = array() + ) { + if (\is_string($body)) { + $body = new BufferedBody($body); + } elseif ($body instanceof ReadableStreamInterface && !$body instanceof StreamInterface) { + $temp = new self($method, '', $headers); + $size = (int) $temp->getHeaderLine('Content-Length'); + if (\strtolower($temp->getHeaderLine('Transfer-Encoding')) === 'chunked') { + $size = null; + } + $body = new HttpBodyStream($body, $size); + } elseif (!$body instanceof StreamInterface) { + throw new \InvalidArgumentException('Invalid server request body given'); + } + + parent::__construct($method, $url, $headers, $body, $version); + + $this->serverParams = $serverParams; + + $query = $this->getUri()->getQuery(); + if ($query !== '') { + \parse_str($query, $this->queryParams); + } + + // Multiple cookie headers are not allowed according + // to https://tools.ietf.org/html/rfc6265#section-5.4 + $cookieHeaders = $this->getHeader("Cookie"); + + if (count($cookieHeaders) === 1) { + $this->cookies = $this->parseCookie($cookieHeaders[0]); + } + } + + public function getServerParams() + { + return $this->serverParams; + } + + public function getCookieParams() + { + return $this->cookies; + } + + public function withCookieParams(array $cookies) + { + $new = clone $this; + $new->cookies = $cookies; + return $new; + } + + public function getQueryParams() + { + return $this->queryParams; + } + + public function withQueryParams(array $query) + { + $new = clone $this; + $new->queryParams = $query; + return $new; + } + + public function getUploadedFiles() + { + return $this->fileParams; + } + + public function withUploadedFiles(array $uploadedFiles) + { + $new = clone $this; + $new->fileParams = $uploadedFiles; + return $new; + } + + public function getParsedBody() + { + return $this->parsedBody; + } + + public function withParsedBody($data) + { + $new = clone $this; + $new->parsedBody = $data; + return $new; + } + + public function getAttributes() + { + return $this->attributes; + } + + public function getAttribute($name, $default = null) + { + if (!\array_key_exists($name, $this->attributes)) { + return $default; + } + return $this->attributes[$name]; + } + + public function withAttribute($name, $value) + { + $new = clone $this; + $new->attributes[$name] = $value; + return $new; + } + + public function withoutAttribute($name) + { + $new = clone $this; + unset($new->attributes[$name]); + return $new; + } + + /** + * @param string $cookie + * @return array + */ + private function parseCookie($cookie) + { + $cookieArray = \explode(';', $cookie); + $result = array(); + + foreach ($cookieArray as $pair) { + $pair = \trim($pair); + $nameValuePair = \explode('=', $pair, 2); + + if (\count($nameValuePair) === 2) { + $key = $nameValuePair[0]; + $value = \urldecode($nameValuePair[1]); + $result[$key] = $value; + } + } + + return $result; + } + + /** + * [Internal] Parse incoming HTTP protocol message + * + * @internal + * @param string $message + * @param array $serverParams + * @return self + * @throws \InvalidArgumentException if given $message is not a valid HTTP request message + */ + public static function parseMessage($message, array $serverParams) + { + // parse request line like "GET /path HTTP/1.1" + $start = array(); + if (!\preg_match('#^(?[^ ]+) (?[^ ]+) HTTP/(?\d\.\d)#m', $message, $start)) { + throw new \InvalidArgumentException('Unable to parse invalid request-line'); + } + + // only support HTTP/1.1 and HTTP/1.0 requests + if ($start['version'] !== '1.1' && $start['version'] !== '1.0') { + throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED); + } + + // check number of valid header fields matches number of lines + request line + $matches = array(); + $n = \preg_match_all(self::REGEX_HEADERS, $message, $matches, \PREG_SET_ORDER); + if (\substr_count($message, "\n") !== $n + 1) { + throw new \InvalidArgumentException('Unable to parse invalid request header fields'); + } + + // format all header fields into associative array + $host = null; + $headers = array(); + foreach ($matches as $match) { + $headers[$match[1]][] = $match[2]; + + // match `Host` request header + if ($host === null && \strtolower($match[1]) === 'host') { + $host = $match[2]; + } + } + + // scheme is `http` unless TLS is used + $scheme = isset($serverParams['HTTPS']) ? 'https://' : 'http://'; + + // default host if unset comes from local socket address or defaults to localhost + $hasHost = $host !== null; + if ($host === null) { + $host = isset($serverParams['SERVER_ADDR'], $serverParams['SERVER_PORT']) ? $serverParams['SERVER_ADDR'] . ':' . $serverParams['SERVER_PORT'] : '127.0.0.1'; + } + + if ($start['method'] === 'OPTIONS' && $start['target'] === '*') { + // support asterisk-form for `OPTIONS *` request line only + $uri = $scheme . $host; + } elseif ($start['method'] === 'CONNECT') { + $parts = \parse_url('tcp://' . $start['target']); + + // check this is a valid authority-form request-target (host:port) + if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) { + throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target'); + } + $uri = $scheme . $start['target']; + } else { + // support absolute-form or origin-form for proxy requests + if ($start['target'][0] === '/') { + $uri = $scheme . $host . $start['target']; + } else { + // ensure absolute-form request-target contains a valid URI + $parts = \parse_url($start['target']); + + // make sure value contains valid host component (IP or hostname), but no fragment + if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) { + throw new \InvalidArgumentException('Invalid absolute-form request-target'); + } + + $uri = $start['target']; + } + } + + $request = new self( + $start['method'], + $uri, + $headers, + '', + $start['version'], + $serverParams + ); + + // only assign request target if it is not in origin-form (happy path for most normal requests) + if ($start['target'][0] !== '/') { + $request = $request->withRequestTarget($start['target']); + } + + if ($hasHost) { + // Optional Host request header value MUST be valid (host and optional port) + $parts = \parse_url('http://' . $request->getHeaderLine('Host')); + + // make sure value contains valid host component (IP or hostname) + if (!$parts || !isset($parts['scheme'], $parts['host'])) { + $parts = false; + } + + // make sure value does not contain any other URI component + if (\is_array($parts)) { + unset($parts['scheme'], $parts['host'], $parts['port']); + } + if ($parts === false || $parts) { + throw new \InvalidArgumentException('Invalid Host header value'); + } + } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') { + // require Host request header for HTTP/1.1 (except for CONNECT method) + throw new \InvalidArgumentException('Missing required Host request header'); + } elseif (!$hasHost) { + // remove default Host request header for HTTP/1.0 when not explicitly given + $request = $request->withoutHeader('Host'); + } + + // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers + if ($request->hasHeader('Transfer-Encoding')) { + if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') { + throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED); + } + + // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time + // as per https://tools.ietf.org/html/rfc7230#section-3.3.3 + if ($request->hasHeader('Content-Length')) { + throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST); + } + } elseif ($request->hasHeader('Content-Length')) { + $string = $request->getHeaderLine('Content-Length'); + + if ((string)(int)$string !== $string) { + // Content-Length value is not an integer or not a single integer + throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST); + } + } + + return $request; + } +} diff --git a/vendor/react/http/src/Message/Uri.php b/vendor/react/http/src/Message/Uri.php new file mode 100644 index 0000000..1eaf24f --- /dev/null +++ b/vendor/react/http/src/Message/Uri.php @@ -0,0 +1,356 @@ +scheme = \strtolower($parts['scheme']); + } + + if (isset($parts['user']) || isset($parts['pass'])) { + $this->userInfo = $this->encode(isset($parts['user']) ? $parts['user'] : '', \PHP_URL_USER) . (isset($parts['pass']) ? ':' . $this->encode($parts['pass'], \PHP_URL_PASS) : ''); + } + + if (isset($parts['host'])) { + $this->host = \strtolower($parts['host']); + } + + if (isset($parts['port']) && !(($parts['port'] === 80 && $this->scheme === 'http') || ($parts['port'] === 443 && $this->scheme === 'https'))) { + $this->port = $parts['port']; + } + + if (isset($parts['path'])) { + $this->path = $this->encode($parts['path'], \PHP_URL_PATH); + } + + if (isset($parts['query'])) { + $this->query = $this->encode($parts['query'], \PHP_URL_QUERY); + } + + if (isset($parts['fragment'])) { + $this->fragment = $this->encode($parts['fragment'], \PHP_URL_FRAGMENT); + } + } + + public function getScheme() + { + return $this->scheme; + } + + public function getAuthority() + { + if ($this->host === '') { + return ''; + } + + return ($this->userInfo !== '' ? $this->userInfo . '@' : '') . $this->host . ($this->port !== null ? ':' . $this->port : ''); + } + + public function getUserInfo() + { + return $this->userInfo; + } + + public function getHost() + { + return $this->host; + } + + public function getPort() + { + return $this->port; + } + + public function getPath() + { + return $this->path; + } + + public function getQuery() + { + return $this->query; + } + + public function getFragment() + { + return $this->fragment; + } + + public function withScheme($scheme) + { + $scheme = \strtolower($scheme); + if ($scheme === $this->scheme) { + return $this; + } + + if (!\preg_match('#^[a-z]*$#', $scheme)) { + throw new \InvalidArgumentException('Invalid URI scheme given'); + } + + $new = clone $this; + $new->scheme = $scheme; + + if (($this->port === 80 && $scheme === 'http') || ($this->port === 443 && $scheme === 'https')) { + $new->port = null; + } + + return $new; + } + + public function withUserInfo($user, $password = null) + { + $userInfo = $this->encode($user, \PHP_URL_USER) . ($password !== null ? ':' . $this->encode($password, \PHP_URL_PASS) : ''); + if ($userInfo === $this->userInfo) { + return $this; + } + + $new = clone $this; + $new->userInfo = $userInfo; + + return $new; + } + + public function withHost($host) + { + $host = \strtolower($host); + if ($host === $this->host) { + return $this; + } + + if (\preg_match('#[\s%+]#', $host) || ($host !== '' && \parse_url('http://' . $host, \PHP_URL_HOST) !== $host)) { + throw new \InvalidArgumentException('Invalid URI host given'); + } + + $new = clone $this; + $new->host = $host; + + return $new; + } + + public function withPort($port) + { + $port = $port === null ? null : (int) $port; + if (($port === 80 && $this->scheme === 'http') || ($port === 443 && $this->scheme === 'https')) { + $port = null; + } + + if ($port === $this->port) { + return $this; + } + + if ($port !== null && ($port < 1 || $port > 0xffff)) { + throw new \InvalidArgumentException('Invalid URI port given'); + } + + $new = clone $this; + $new->port = $port; + + return $new; + } + + public function withPath($path) + { + $path = $this->encode($path, \PHP_URL_PATH); + if ($path === $this->path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + + return $new; + } + + public function withQuery($query) + { + $query = $this->encode($query, \PHP_URL_QUERY); + if ($query === $this->query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + + return $new; + } + + public function withFragment($fragment) + { + $fragment = $this->encode($fragment, \PHP_URL_FRAGMENT); + if ($fragment === $this->fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + + return $new; + } + + public function __toString() + { + $uri = ''; + if ($this->scheme !== '') { + $uri .= $this->scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== '') { + $uri .= '//' . $authority; + } + + if ($authority !== '' && isset($this->path[0]) && $this->path[0] !== '/') { + $uri .= '/' . $this->path; + } elseif ($authority === '' && isset($this->path[0]) && $this->path[0] === '/') { + $uri .= '/' . \ltrim($this->path, '/'); + } else { + $uri .= $this->path; + } + + if ($this->query !== '') { + $uri .= '?' . $this->query; + } + + if ($this->fragment !== '') { + $uri .= '#' . $this->fragment; + } + + return $uri; + } + + /** + * @param string $part + * @param int $component + * @return string + */ + private function encode($part, $component) + { + return \preg_replace_callback( + '/(?:[^a-z0-9_\-\.~!\$&\'\(\)\*\+,;=' . ($component === \PHP_URL_PATH ? ':@\/' : ($component === \PHP_URL_QUERY || $component === \PHP_URL_FRAGMENT ? ':@\/\?' : '')) . '%]++|%(?![a-f0-9]{2}))/i', + function (array $match) { + return \rawurlencode($match[0]); + }, + $part + ); + } + + /** + * [Internal] Resolve URI relative to base URI and return new absolute URI + * + * @internal + * @param UriInterface $base + * @param UriInterface $rel + * @return UriInterface + * @throws void + */ + public static function resolve(UriInterface $base, UriInterface $rel) + { + if ($rel->getScheme() !== '') { + return $rel->getPath() === '' ? $rel : $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + $reset = false; + $new = $base; + if ($rel->getAuthority() !== '') { + $reset = true; + $userInfo = \explode(':', $rel->getUserInfo(), 2); + $new = $base->withUserInfo($userInfo[0], isset($userInfo[1]) ? $userInfo[1]: null)->withHost($rel->getHost())->withPort($rel->getPort()); + } + + if ($reset && $rel->getPath() === '') { + $new = $new->withPath(''); + } elseif (($path = $rel->getPath()) !== '') { + $start = ''; + if ($path === '' || $path[0] !== '/') { + $start = $base->getPath(); + if (\substr($start, -1) !== '/') { + $start .= '/../'; + } + } + $reset = true; + $new = $new->withPath(self::removeDotSegments($start . $path)); + } + if ($reset || $rel->getQuery() !== '') { + $reset = true; + $new = $new->withQuery($rel->getQuery()); + } + if ($reset || $rel->getFragment() !== '') { + $new = $new->withFragment($rel->getFragment()); + } + + return $new; + } + + /** + * @param string $path + * @return string + */ + private static function removeDotSegments($path) + { + $segments = array(); + foreach (\explode('/', $path) as $segment) { + if ($segment === '..') { + \array_pop($segments); + } elseif ($segment !== '.' && $segment !== '') { + $segments[] = $segment; + } + } + return '/' . \implode('/', $segments) . ($path !== '/' && \substr($path, -1) === '/' ? '/' : ''); + } +} diff --git a/vendor/react/http/src/Middleware/LimitConcurrentRequestsMiddleware.php b/vendor/react/http/src/Middleware/LimitConcurrentRequestsMiddleware.php new file mode 100644 index 0000000..b1c00da --- /dev/null +++ b/vendor/react/http/src/Middleware/LimitConcurrentRequestsMiddleware.php @@ -0,0 +1,211 @@ +limit = $limit; + } + + public function __invoke(ServerRequestInterface $request, $next) + { + // happy path: simply invoke next request handler if we're below limit + if ($this->pending < $this->limit) { + ++$this->pending; + + try { + $response = $next($request); + } catch (\Exception $e) { + $this->processQueue(); + throw $e; + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // handle Errors just like Exceptions (PHP 7+ only) + $this->processQueue(); + throw $e; // @codeCoverageIgnoreEnd + } + + // happy path: if next request handler returned immediately, + // we can simply try to invoke the next queued request + if ($response instanceof ResponseInterface) { + $this->processQueue(); + return $response; + } + + // if the next handler returns a pending promise, we have to + // await its resolution before invoking next queued request + return $this->await(Promise\resolve($response)); + } + + // if we reach this point, then this request will need to be queued + // check if the body is streaming, in which case we need to buffer everything + $body = $request->getBody(); + if ($body instanceof ReadableStreamInterface) { + // pause actual body to stop emitting data until the handler is called + $size = $body->getSize(); + $body = new PauseBufferStream($body); + $body->pauseImplicit(); + + // replace with buffering body to ensure any readable events will be buffered + $request = $request->withBody(new HttpBodyStream( + $body, + $size + )); + } + + // get next queue position + $queue =& $this->queue; + $queue[] = null; + \end($queue); + $id = \key($queue); + + $deferred = new Deferred(function ($_, $reject) use (&$queue, $id) { + // queued promise cancelled before its next handler is invoked + // remove from queue and reject explicitly + unset($queue[$id]); + $reject(new \RuntimeException('Cancelled queued next handler')); + }); + + // queue request and process queue if pending does not exceed limit + $queue[$id] = $deferred; + + $pending = &$this->pending; + $that = $this; + return $deferred->promise()->then(function () use ($request, $next, $body, &$pending, $that) { + // invoke next request handler + ++$pending; + + try { + $response = $next($request); + } catch (\Exception $e) { + $that->processQueue(); + throw $e; + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // handle Errors just like Exceptions (PHP 7+ only) + $that->processQueue(); + throw $e; // @codeCoverageIgnoreEnd + } + + // resume readable stream and replay buffered events + if ($body instanceof PauseBufferStream) { + $body->resumeImplicit(); + } + + // if the next handler returns a pending promise, we have to + // await its resolution before invoking next queued request + return $that->await(Promise\resolve($response)); + }); + } + + /** + * @internal + * @param PromiseInterface $promise + * @return PromiseInterface + */ + public function await(PromiseInterface $promise) + { + $that = $this; + + return $promise->then(function ($response) use ($that) { + $that->processQueue(); + + return $response; + }, function ($error) use ($that) { + $that->processQueue(); + + return Promise\reject($error); + }); + } + + /** + * @internal + */ + public function processQueue() + { + // skip if we're still above concurrency limit or there's no queued request waiting + if (--$this->pending >= $this->limit || !$this->queue) { + return; + } + + $first = \reset($this->queue); + unset($this->queue[key($this->queue)]); + + $first->resolve(null); + } +} diff --git a/vendor/react/http/src/Middleware/RequestBodyBufferMiddleware.php b/vendor/react/http/src/Middleware/RequestBodyBufferMiddleware.php new file mode 100644 index 0000000..ddb39f5 --- /dev/null +++ b/vendor/react/http/src/Middleware/RequestBodyBufferMiddleware.php @@ -0,0 +1,109 @@ +sizeLimit = IniUtil::iniSizeToBytes($sizeLimit); + } + + public function __invoke(ServerRequestInterface $request, $next) + { + $body = $request->getBody(); + $size = $body->getSize(); + + // happy path: skip if body is known to be empty (or is already buffered) + if ($size === 0 || !$body instanceof ReadableStreamInterface || !$body->isReadable()) { + // replace with empty body if body is streaming (or buffered size exceeds limit) + if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) { + $request = $request->withBody(new BufferedBody('')); + } + + return $next($request); + } + + // request body of known size exceeding limit + $sizeLimit = $this->sizeLimit; + if ($size > $this->sizeLimit) { + $sizeLimit = 0; + } + + /** @var ?\Closure $closer */ + $closer = null; + + return new Promise(function ($resolve, $reject) use ($body, &$closer, $sizeLimit, $request, $next) { + // buffer request body data in memory, discard but keep buffering if limit is reached + $buffer = ''; + $bufferer = null; + $body->on('data', $bufferer = function ($data) use (&$buffer, $sizeLimit, $body, &$bufferer) { + $buffer .= $data; + + // On buffer overflow keep the request body stream in, + // but ignore the contents and wait for the close event + // before passing the request on to the next middleware. + if (isset($buffer[$sizeLimit])) { + assert($bufferer instanceof \Closure); + $body->removeListener('data', $bufferer); + $bufferer = null; + $buffer = ''; + } + }); + + // call $next with current buffer and resolve or reject with its results + $body->on('close', $closer = function () use (&$buffer, $request, $resolve, $reject, $next) { + try { + // resolve with result of next handler + $resolve($next($request->withBody(new BufferedBody($buffer)))); + } catch (\Exception $e) { + $reject($e); + } catch (\Throwable $e) { // @codeCoverageIgnoreStart + // reject Errors just like Exceptions (PHP 7+) + $reject($e); // @codeCoverageIgnoreEnd + } + }); + + // reject buffering if body emits error + $body->on('error', function (\Exception $e) use ($reject, $body, $closer) { + // remove close handler to avoid resolving, then close and reject + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); + + $reject(new \RuntimeException( + 'Error while buffering request body: ' . $e->getMessage(), + $e->getCode(), + $e + )); + }); + }, function () use ($body, &$closer) { + // cancelled buffering: remove close handler to avoid resolving, then close and reject + assert($closer instanceof \Closure); + $body->removeListener('close', $closer); + $body->close(); + + throw new \RuntimeException('Cancelled buffering request body'); + }); + } +} diff --git a/vendor/react/http/src/Middleware/RequestBodyParserMiddleware.php b/vendor/react/http/src/Middleware/RequestBodyParserMiddleware.php new file mode 100644 index 0000000..be5ba16 --- /dev/null +++ b/vendor/react/http/src/Middleware/RequestBodyParserMiddleware.php @@ -0,0 +1,46 @@ +multipart = new MultipartParser($uploadMaxFilesize, $maxFileUploads); + } + + public function __invoke(ServerRequestInterface $request, $next) + { + $type = \strtolower($request->getHeaderLine('Content-Type')); + list ($type) = \explode(';', $type); + + if ($type === 'application/x-www-form-urlencoded') { + return $next($this->parseFormUrlencoded($request)); + } + + if ($type === 'multipart/form-data') { + return $next($this->multipart->parse($request)); + } + + return $next($request); + } + + private function parseFormUrlencoded(ServerRequestInterface $request) + { + // parse string into array structure + // ignore warnings due to excessive data structures (max_input_vars and max_input_nesting_level) + $ret = array(); + @\parse_str((string)$request->getBody(), $ret); + + return $request->withParsedBody($ret); + } +} diff --git a/vendor/react/http/src/Middleware/StreamingRequestMiddleware.php b/vendor/react/http/src/Middleware/StreamingRequestMiddleware.php new file mode 100644 index 0000000..6ab74b7 --- /dev/null +++ b/vendor/react/http/src/Middleware/StreamingRequestMiddleware.php @@ -0,0 +1,69 @@ +getBody(); + * assert($body instanceof Psr\Http\Message\StreamInterface); + * assert($body instanceof React\Stream\ReadableStreamInterface); + * + * return new React\Promise\Promise(function ($resolve) use ($body) { + * $bytes = 0; + * $body->on('data', function ($chunk) use (&$bytes) { + * $bytes += \count($chunk); + * }); + * $body->on('close', function () use (&$bytes, $resolve) { + * $resolve(new React\Http\Response( + * 200, + * [], + * "Received $bytes bytes\n" + * )); + * }); + * }); + * } + * ); + * ``` + * + * See also [streaming incoming request](../../README.md#streaming-incoming-request) + * for more details. + * + * Additionally, this middleware can be used in combination with the + * [`LimitConcurrentRequestsMiddleware`](#limitconcurrentrequestsmiddleware) and + * [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware) (see below) + * to explicitly configure the total number of requests that can be handled at + * once: + * + * ```php + * $http = new React\Http\HttpServer( + * new React\Http\Middleware\StreamingRequestMiddleware(), + * new React\Http\Middleware\LimitConcurrentRequestsMiddleware(100), // 100 concurrent buffering handlers + * new React\Http\Middleware\RequestBodyBufferMiddleware(2 * 1024 * 1024), // 2 MiB per request + * new React\Http\Middleware\RequestBodyParserMiddleware(), + * $handler + * ); + * ``` + * + * > Internally, this class is used as a "marker" to not trigger the default + * request buffering behavior in the `HttpServer`. It does not implement any logic + * on its own. + */ +final class StreamingRequestMiddleware +{ + public function __invoke(ServerRequestInterface $request, $next) + { + return $next($request); + } +} diff --git a/vendor/react/http/src/Server.php b/vendor/react/http/src/Server.php new file mode 100644 index 0000000..9bb9cf7 --- /dev/null +++ b/vendor/react/http/src/Server.php @@ -0,0 +1,18 @@ +connect($uri)->then(function (React\Socket\ConnectionInterface $conn) { + // … + }, function (Exception $e) { + echo 'Error:' . $e->getMessage() . PHP_EOL; + }); + ``` + +* Improve test suite, test against PHP 8.1 release. + (#274 by @SimonFrings) + +## 1.9.0 (2021-08-03) + +* Feature: Add new `SocketServer` and deprecate `Server` to avoid class name collisions. + (#263 by @clue) + + The new `SocketServer` class has been added with an improved constructor signature + as a replacement for the previous `Server` class in order to avoid any ambiguities. + The previous name has been deprecated and should not be used anymore. + In its most basic form, the deprecated `Server` can now be considered an alias for new `SocketServer`. + + ```php + // deprecated + $socket = new React\Socket\Server(0); + $socket = new React\Socket\Server('127.0.0.1:8000'); + $socket = new React\Socket\Server('127.0.0.1:8000', null, $context); + $socket = new React\Socket\Server('127.0.0.1:8000', $loop, $context); + + // new + $socket = new React\Socket\SocketServer('127.0.0.1:0'); + $socket = new React\Socket\SocketServer('127.0.0.1:8000'); + $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context); + $socket = new React\Socket\SocketServer('127.0.0.1:8000', $context, $loop); + ``` + +* Feature: Update `Connector` signature to take optional `$context` as first argument. + (#264 by @clue) + + The new signature has been added to match the new `SocketServer` and + consistently move the now commonly unneeded loop argument to the last argument. + The previous signature has been deprecated and should not be used anymore. + In its most basic form, both signatures are compatible. + + ```php + // deprecated + $connector = new React\Socket\Connector(null, $context); + $connector = new React\Socket\Connector($loop, $context); + + // new + $connector = new React\Socket\Connector($context); + $connector = new React\Socket\Connector($context, $loop); + ``` + +## 1.8.0 (2021-07-11) + +A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop). + +* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop). + (#260 by @clue) + + ```php + // old (still supported) + $socket = new React\Socket\Server('127.0.0.1:8080', $loop); + $connector = new React\Socket\Connector($loop); + + // new (using default loop) + $socket = new React\Socket\Server('127.0.0.1:8080'); + $connector = new React\Socket\Connector(); + ``` + +## 1.7.0 (2021-06-25) + +* Feature: Support falling back to multiple DNS servers from DNS config. + (#257 by @clue) + + If you're using the default `Connector`, it will now use all DNS servers + configured on your system. If you have multiple DNS servers configured and + connectivity to the primary DNS server is broken, it will now fall back to + your other DNS servers, thus providing improved connectivity and redundancy + for broken DNS configurations. + +* Feature: Use round robin for happy eyeballs DNS responses (load balancing). + (#247 by @clue) + + If you're using the default `Connector`, it will now randomize the order of + the IP addresses resolved via DNS when connecting. This allows the load to + be distributed more evenly across all returned IP addresses. This can be + used as a very basic DNS load balancing mechanism. + +* Internal improvement to avoid unhandled rejection for future Promise API. + (#258 by @clue) + +* Improve test suite, use GitHub actions for continuous integration (CI). + (#254 by @SimonFrings) + +## 1.6.0 (2020-08-28) + +* Feature: Support upcoming PHP 8 release. + (#246 by @clue) + +* Feature: Change default socket backlog size to 511. + (#242 by @clue) + +* Fix: Fix closing connection when cancelling during TLS handshake. + (#241 by @clue) + +* Fix: Fix blocking during possible `accept()` race condition + when multiple socket servers listen on same socket address. + (#244 by @clue) + +* Improve test suite, update PHPUnit config and add full core team to the license. + (#243 by @SimonFrings and #245 by @WyriHaximus) + +## 1.5.0 (2020-07-01) + +* Feature / Fix: Improve error handling and reporting for happy eyeballs and + immediately try next connection when one connection attempt fails. + (#230, #231, #232 and #233 by @clue) + + Error messages for failed connection attempts now include more details to + ease debugging. Additionally, the happy eyeballs algorithm has been improved + to avoid having to wait for some timers to expire which significantly + improves connection setup times (in particular when IPv6 isn't available). + +* Improve test suite, minor code cleanup and improve code coverage to 100%. + Update to PHPUnit 9 and skip legacy TLS 1.0 / TLS 1.1 tests if disabled by + system. Run tests on Windows and simplify Travis CI test matrix for Mac OS X + setup and skip all TLS tests on legacy HHVM. + (#229, #235, #236 and #238 by @clue and #239 by @SimonFrings) + +## 1.4.0 (2020-03-12) + +A major new feature release, see [**release announcement**](https://clue.engineering/2020/introducing-ipv6-for-reactphp). + +* Feature: Add IPv6 support to `Connector` (implement "Happy Eyeballs" algorithm to support IPv6 probing). + IPv6 support is turned on by default, use new `happy_eyeballs` option in `Connector` to toggle behavior. + (#196, #224 and #225 by @WyriHaximus and @clue) + +* Feature: Default to using DNS cache (with max 256 entries) for `Connector`. + (#226 by @clue) + +* Add `.gitattributes` to exclude dev files from exports and some minor code style fixes. + (#219 by @reedy and #218 by @mmoreram) + +* Improve test suite to fix failing test cases when using new DNS component, + significantly improve test performance by awaiting events instead of sleeping, + exclude TLS 1.3 test on PHP 7.3, run tests on PHP 7.4 and simplify test matrix. + (#208, #209, #210, #217 and #223 by @clue) + +## 1.3.0 (2019-07-10) + +* Feature: Forward compatibility with upcoming stable DNS component. + (#206 by @clue) + +## 1.2.1 (2019-06-03) + +* Avoid uneeded fragmented TLS work around for PHP 7.3.3+ and + work around failing test case detecting EOF on TLS 1.3 socket streams. + (#201 and #202 by @clue) + +* Improve TLS certificate/passphrase example. + (#190 by @jsor) + +## 1.2.0 (2019-01-07) + +* Feature / Fix: Improve TLS 1.3 support. + (#186 by @clue) + + TLS 1.3 is now an official standard as of August 2018! :tada: + The protocol has major improvements in the areas of security, performance, and privacy. + TLS 1.3 is supported by default as of [OpenSSL 1.1.1](https://www.openssl.org/blog/blog/2018/09/11/release111/). + For example, this version ships with Ubuntu 18.10 (and newer) by default, meaning that recent installations support TLS 1.3 out of the box :shipit: + +* Fix: Avoid possibility of missing remote address when TLS handshake fails. + (#188 by @clue) + +* Improve performance by prefixing all global functions calls with `\` to skip the look up and resolve process and go straight to the global function. + (#183 by @WyriHaximus) + +* Update documentation to use full class names with namespaces. + (#187 by @clue) + +* Improve test suite to avoid some possible race conditions, + test against PHP 7.3 on Travis and + use dedicated `assertInstanceOf()` assertions. + (#185 by @clue, #178 by @WyriHaximus and #181 by @carusogabriel) + +## 1.1.0 (2018-10-01) + +* Feature: Improve error reporting for failed connection attempts and improve + cancellation forwarding during DNS lookup, TCP/IP connection or TLS handshake. + (#168, #169, #170, #171, #176 and #177 by @clue) + + All error messages now always contain a reference to the remote URI to give + more details which connection actually failed and the reason for this error. + Accordingly, failures during DNS lookup will now mention both the remote URI + as well as the DNS error reason. TCP/IP connection issues and errors during + a secure TLS handshake will both mention the remote URI as well as the + underlying socket error. Similarly, lost/dropped connections during a TLS + handshake will now report a lost connection instead of an empty error reason. + + For most common use cases this means that simply reporting the `Exception` + message should give the most relevant details for any connection issues: + + ```php + $promise = $connector->connect('tls://example.com:443'); + $promise->then(function (ConnectionInterface $conn) use ($loop) { + // … + }, function (Exception $e) { + echo $e->getMessage(); + }); + ``` + +## 1.0.0 (2018-07-11) + +* First stable LTS release, now following [SemVer](https://semver.org/). + We'd like to emphasize that this component is production ready and battle-tested. + We plan to support all long-term support (LTS) releases for at least 24 months, + so you have a rock-solid foundation to build on top of. + +> Contains no other changes, so it's actually fully compatible with the v0.8.12 release. + +## 0.8.12 (2018-06-11) + +* Feature: Improve memory consumption for failed and cancelled connection attempts. + (#161 by @clue) + +* Improve test suite to fix Travis config to test against legacy PHP 5.3 again. + (#162 by @clue) + +## 0.8.11 (2018-04-24) + +* Feature: Improve memory consumption for cancelled connection attempts and + simplify skipping DNS lookup when connecting to IP addresses. + (#159 and #160 by @clue) + +## 0.8.10 (2018-02-28) + +* Feature: Update DNS dependency to support loading system default DNS + nameserver config on all supported platforms + (`/etc/resolv.conf` on Unix/Linux/Mac/Docker/WSL and WMIC on Windows) + (#152 by @clue) + + This means that connecting to hosts that are managed by a local DNS server, + such as a corporate DNS server or when using Docker containers, will now + work as expected across all platforms with no changes required: + + ```php + $connector = new Connector($loop); + $connector->connect('intranet.example:80')->then(function ($connection) { + // … + }); + ``` + +## 0.8.9 (2018-01-18) + +* Feature: Support explicitly choosing TLS version to negotiate with remote side + by respecting `crypto_method` context parameter for all classes. + (#149 by @clue) + + By default, all connector and server classes support TLSv1.0+ and exclude + support for legacy SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly + choose the TLS version you want to negotiate with the remote side: + + ```php + // new: now supports 'crypto_method` context parameter for all classes + $connector = new Connector($loop, array( + 'tls' => array( + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + ) + )); + ``` + +* Minor internal clean up to unify class imports + (#148 by @clue) + +## 0.8.8 (2018-01-06) + +* Improve test suite by adding test group to skip integration tests relying on + internet connection and fix minor documentation typo. + (#146 by @clue and #145 by @cn007b) + +## 0.8.7 (2017-12-24) + +* Fix: Fix closing socket resource before removing from loop + (#141 by @clue) + + This fixes the root cause of an uncaught `Exception` that only manifested + itself after the recent Stream v0.7.4 component update and only if you're + using `ext-event` (`ExtEventLoop`). + +* Improve test suite by testing against PHP 7.2 + (#140 by @carusogabriel) + +## 0.8.6 (2017-11-18) + +* Feature: Add Unix domain socket (UDS) support to `Server` with `unix://` URI scheme + and add advanced `UnixServer` class. + (#120 by @andig) + + ```php + // new: Server now supports "unix://" scheme + $server = new Server('unix:///tmp/server.sock', $loop); + + // new: advanced usage + $server = new UnixServer('/tmp/server.sock', $loop); + ``` + +* Restructure examples to ease getting started + (#136 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit 6 and + ignore Mac OS X test failures for now until Travis tests work again + (#133 by @gabriel-caruso and #134 by @clue) + +## 0.8.5 (2017-10-23) + +* Fix: Work around PHP bug with Unix domain socket (UDS) paths for Mac OS X + (#123 by @andig) + +* Fix: Fix `SecureServer` to return `null` URI if server socket is already closed + (#129 by @clue) + +* Improve test suite by adding forward compatibility with PHPUnit v5 and + forward compatibility with upcoming EventLoop releases in tests and + test Mac OS X on Travis + (#122 by @andig and #125, #127 and #130 by @clue) + +* Readme improvements + (#118 by @jsor) + +## 0.8.4 (2017-09-16) + +* Feature: Add `FixedUriConnector` decorator to use fixed, preconfigured URI instead + (#117 by @clue) + + This can be useful for consumers that do not support certain URIs, such as + when you want to explicitly connect to a Unix domain socket (UDS) path + instead of connecting to a default address assumed by an higher-level API: + + ```php + $connector = new FixedUriConnector( + 'unix:///var/run/docker.sock', + new UnixConnector($loop) + ); + + // destination will be ignored, actually connects to Unix domain socket + $promise = $connector->connect('localhost:80'); + ``` + +## 0.8.3 (2017-09-08) + +* Feature: Reduce memory consumption for failed connections + (#113 by @valga) + +* Fix: Work around write chunk size for TLS streams for PHP < 7.1.14 + (#114 by @clue) + +## 0.8.2 (2017-08-25) + +* Feature: Update DNS dependency to support hosts file on all platforms + (#112 by @clue) + + This means that connecting to hosts such as `localhost` will now work as + expected across all platforms with no changes required: + + ```php + $connector = new Connector($loop); + $connector->connect('localhost:8080')->then(function ($connection) { + // … + }); + ``` + +## 0.8.1 (2017-08-15) + +* Feature: Forward compatibility with upcoming EventLoop v1.0 and v0.5 and + target evenement 3.0 a long side 2.0 and 1.0 + (#104 by @clue and #111 by @WyriHaximus) + +* Improve test suite by locking Travis distro so new defaults will not break the build and + fix HHVM build for now again and ignore future HHVM build errors + (#109 and #110 by @clue) + +* Minor documentation fixes + (#103 by @christiaan and #108 by @hansott) + +## 0.8.0 (2017-05-09) + +* Feature: New `Server` class now acts as a facade for existing server classes + and renamed old `Server` to `TcpServer` for advanced usage. + (#96 and #97 by @clue) + + The `Server` class is now the main class in this package that implements the + `ServerInterface` and allows you to accept incoming streaming connections, + such as plaintext TCP/IP or secure TLS connection streams. + + > This is not a BC break and consumer code does not have to be updated. + +* Feature / BC break: All addresses are now URIs that include the URI scheme + (#98 by @clue) + + ```diff + - $parts = parse_url('tcp://' . $conn->getRemoteAddress()); + + $parts = parse_url($conn->getRemoteAddress()); + ``` + +* Fix: Fix `unix://` addresses for Unix domain socket (UDS) paths + (#100 by @clue) + +* Feature: Forward compatibility with Stream v1.0 and v0.7 + (#99 by @clue) + +## 0.7.2 (2017-04-24) + +* Fix: Work around latest PHP 7.0.18 and 7.1.4 no longer accepting full URIs + (#94 by @clue) + +## 0.7.1 (2017-04-10) + +* Fix: Ignore HHVM errors when closing connection that is already closing + (#91 by @clue) + +## 0.7.0 (2017-04-10) + +* Feature: Merge SocketClient component into this component + (#87 by @clue) + + This means that this package now provides async, streaming plaintext TCP/IP + and secure TLS socket server and client connections for ReactPHP. + + ``` + $connector = new React\Socket\Connector($loop); + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $connection->write('…'); + }); + ``` + + Accordingly, the `ConnectionInterface` is now used to represent both incoming + server side connections as well as outgoing client side connections. + + If you've previously used the SocketClient component to establish outgoing + client connections, upgrading should take no longer than a few minutes. + All classes have been merged as-is from the latest `v0.7.0` release with no + other changes, so you can simply update your code to use the updated namespace + like this: + + ```php + // old from SocketClient component and namespace + $connector = new React\SocketClient\Connector($loop); + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $connection->write('…'); + }); + + // new + $connector = new React\Socket\Connector($loop); + $connector->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $connection->write('…'); + }); + ``` + +## 0.6.0 (2017-04-04) + +* Feature: Add `LimitingServer` to limit and keep track of open connections + (#86 by @clue) + + ```php + $server = new Server(0, $loop); + $server = new LimitingServer($server, 100); + + $server->on('connection', function (ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … + }); + ``` + +* Feature / BC break: Add `pause()` and `resume()` methods to limit active + connections + (#84 by @clue) + + ```php + $server = new Server(0, $loop); + $server->pause(); + + $loop->addTimer(1.0, function() use ($server) { + $server->resume(); + }); + ``` + +## 0.5.1 (2017-03-09) + +* Feature: Forward compatibility with Stream v0.5 and upcoming v0.6 + (#79 by @clue) + +## 0.5.0 (2017-02-14) + +* Feature / BC break: Replace `listen()` call with URIs passed to constructor + and reject listening on hostnames with `InvalidArgumentException` + and replace `ConnectionException` with `RuntimeException` for consistency + (#61, #66 and #72 by @clue) + + ```php + // old + $server = new Server($loop); + $server->listen(8080); + + // new + $server = new Server(8080, $loop); + ``` + + Similarly, you can now pass a full listening URI to the constructor to change + the listening host: + + ```php + // old + $server = new Server($loop); + $server->listen(8080, '127.0.0.1'); + + // new + $server = new Server('127.0.0.1:8080', $loop); + ``` + + Trying to start listening on (DNS) host names will now throw an + `InvalidArgumentException`, use IP addresses instead: + + ```php + // old + $server = new Server($loop); + $server->listen(8080, 'localhost'); + + // new + $server = new Server('127.0.0.1:8080', $loop); + ``` + + If trying to listen fails (such as if port is already in use or port below + 1024 may require root access etc.), it will now throw a `RuntimeException`, + the `ConnectionException` class has been removed: + + ```php + // old: throws React\Socket\ConnectionException + $server = new Server($loop); + $server->listen(80); + + // new: throws RuntimeException + $server = new Server(80, $loop); + ``` + +* Feature / BC break: Rename `shutdown()` to `close()` for consistency throughout React + (#62 by @clue) + + ```php + // old + $server->shutdown(); + + // new + $server->close(); + ``` + +* Feature / BC break: Replace `getPort()` with `getAddress()` + (#67 by @clue) + + ```php + // old + echo $server->getPort(); // 8080 + + // new + echo $server->getAddress(); // 127.0.0.1:8080 + ``` + +* Feature / BC break: `getRemoteAddress()` returns full address instead of only IP + (#65 by @clue) + + ```php + // old + echo $connection->getRemoteAddress(); // 192.168.0.1 + + // new + echo $connection->getRemoteAddress(); // 192.168.0.1:51743 + ``` + +* Feature / BC break: Add `getLocalAddress()` method + (#68 by @clue) + + ```php + echo $connection->getLocalAddress(); // 127.0.0.1:8080 + ``` + +* BC break: The `Server` and `SecureServer` class are now marked `final` + and you can no longer `extend` them + (which was never documented or recommended anyway). + Public properties and event handlers are now internal only. + Please use composition instead of extension. + (#71, #70 and #69 by @clue) + +## 0.4.6 (2017-01-26) + +* Feature: Support socket context options passed to `Server` + (#64 by @clue) + +* Fix: Properly return `null` for unknown addresses + (#63 by @clue) + +* Improve documentation for `ServerInterface` and lock test suite requirements + (#60 by @clue, #57 by @shaunbramley) + +## 0.4.5 (2017-01-08) + +* Feature: Add `SecureServer` for secure TLS connections + (#55 by @clue) + +* Add functional integration tests + (#54 by @clue) + +## 0.4.4 (2016-12-19) + +* Feature / Fix: `ConnectionInterface` should extend `DuplexStreamInterface` + documentation + (#50 by @clue) + +* Feature / Fix: Improve test suite and switch to normal stream handler + (#51 by @clue) + +* Feature: Add examples + (#49 by @clue) + +## 0.4.3 (2016-03-01) + +* Bug fix: Suppress errors on stream_socket_accept to prevent PHP from crashing +* Support for PHP7 and HHVM +* Support PHP 5.3 again + +## 0.4.2 (2014-05-25) + +* Verify stream is a valid resource in Connection + +## 0.4.1 (2014-04-13) + +* Bug fix: Check read buffer for data before shutdown signal and end emit (@ArtyDev) +* Bug fix: v0.3.4 changes merged for v0.4.1 + +## 0.3.4 (2014-03-30) + +* Bug fix: Reset socket to non-blocking after shutting down (PHP bug) + +## 0.4.0 (2014-02-02) + +* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks +* BC break: Update to React/Promise 2.0 +* BC break: Update to Evenement 2.0 +* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 +* Bump React dependencies to v0.4 + +## 0.3.3 (2013-07-08) + +* Version bump + +## 0.3.2 (2013-05-10) + +* Version bump + +## 0.3.1 (2013-04-21) + +* Feature: Support binding to IPv6 addresses (@clue) + +## 0.3.0 (2013-04-14) + +* Bump React dependencies to v0.3 + +## 0.2.6 (2012-12-26) + +* Version bump + +## 0.2.3 (2012-11-14) + +* Version bump + +## 0.2.0 (2012-09-10) + +* Bump React dependencies to v0.2 + +## 0.1.1 (2012-07-12) + +* Version bump + +## 0.1.0 (2012-07-11) + +* First tagged release diff --git a/vendor/react/socket/LICENSE b/vendor/react/socket/LICENSE new file mode 100644 index 0000000..d6f8901 --- /dev/null +++ b/vendor/react/socket/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/react/socket/README.md b/vendor/react/socket/README.md new file mode 100644 index 0000000..e77e676 --- /dev/null +++ b/vendor/react/socket/README.md @@ -0,0 +1,1564 @@ +# Socket + +[![CI status](https://github.com/reactphp/socket/workflows/CI/badge.svg)](https://github.com/reactphp/socket/actions) +[![installs on Packagist](https://img.shields.io/packagist/dt/react/socket?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/socket) + +Async, streaming plaintext TCP/IP and secure TLS socket server and client +connections for [ReactPHP](https://reactphp.org/). + +The socket library provides re-usable interfaces for a socket-layer +server and client based on the [`EventLoop`](https://github.com/reactphp/event-loop) +and [`Stream`](https://github.com/reactphp/stream) components. +Its server component allows you to build networking servers that accept incoming +connections from networking clients (such as an HTTP server). +Its client component allows you to build networking clients that establish +outgoing connections to networking servers (such as an HTTP or database client). +This library provides async, streaming means for all of this, so you can +handle multiple concurrent connections without blocking. + +**Table of Contents** + +* [Quickstart example](#quickstart-example) +* [Connection usage](#connection-usage) + * [ConnectionInterface](#connectioninterface) + * [getRemoteAddress()](#getremoteaddress) + * [getLocalAddress()](#getlocaladdress) +* [Server usage](#server-usage) + * [ServerInterface](#serverinterface) + * [connection event](#connection-event) + * [error event](#error-event) + * [getAddress()](#getaddress) + * [pause()](#pause) + * [resume()](#resume) + * [close()](#close) + * [SocketServer](#socketserver) + * [Advanced server usage](#advanced-server-usage) + * [TcpServer](#tcpserver) + * [SecureServer](#secureserver) + * [UnixServer](#unixserver) + * [LimitingServer](#limitingserver) + * [getConnections()](#getconnections) +* [Client usage](#client-usage) + * [ConnectorInterface](#connectorinterface) + * [connect()](#connect) + * [Connector](#connector) + * [Advanced client usage](#advanced-client-usage) + * [TcpConnector](#tcpconnector) + * [HappyEyeBallsConnector](#happyeyeballsconnector) + * [DnsConnector](#dnsconnector) + * [SecureConnector](#secureconnector) + * [TimeoutConnector](#timeoutconnector) + * [UnixConnector](#unixconnector) + * [FixUriConnector](#fixeduriconnector) +* [Install](#install) +* [Tests](#tests) +* [License](#license) + +## Quickstart example + +Here is a server that closes the connection if you send it anything: + +```php +$socket = new React\Socket\SocketServer('127.0.0.1:8080'); + +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { + $connection->write("Hello " . $connection->getRemoteAddress() . "!\n"); + $connection->write("Welcome to this amazing server!\n"); + $connection->write("Here's a tip: don't say anything.\n"); + + $connection->on('data', function ($data) use ($connection) { + $connection->close(); + }); +}); +``` + +See also the [examples](examples). + +Here's a client that outputs the output of said server and then attempts to +send it a string: + +```php +$connector = new React\Socket\Connector(); + +$connector->connect('127.0.0.1:8080')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->pipe(new React\Stream\WritableResourceStream(STDOUT)); + $connection->write("Hello World!\n"); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +## Connection usage + +### ConnectionInterface + +The `ConnectionInterface` is used to represent any incoming and outgoing +connection, such as a normal TCP/IP connection. + +An incoming or outgoing connection is a duplex stream (both readable and +writable) that implements React's +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). +It contains additional properties for the local and remote address (client IP) +where this connection has been established to/from. + +Most commonly, instances implementing this `ConnectionInterface` are emitted +by all classes implementing the [`ServerInterface`](#serverinterface) and +used by all classes implementing the [`ConnectorInterface`](#connectorinterface). + +Because the `ConnectionInterface` implements the underlying +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface) +you can use any of its events and methods as usual: + +```php +$connection->on('data', function ($chunk) { + echo $chunk; +}); + +$connection->on('end', function () { + echo 'ended'; +}); + +$connection->on('error', function (Exception $e) { + echo 'error: ' . $e->getMessage(); +}); + +$connection->on('close', function () { + echo 'closed'; +}); + +$connection->write($data); +$connection->end($data = null); +$connection->close(); +// … +``` + +For more details, see the +[`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + +#### getRemoteAddress() + +The `getRemoteAddress(): ?string` method returns the full remote address +(URI) where this connection has been established with. + +```php +$address = $connection->getRemoteAddress(); +echo 'Connection with ' . $address . PHP_EOL; +``` + +If the remote address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full address (URI) as a string value, such +as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, +`unix://example.sock` or `unix:///path/to/example.sock`. +Note that individual URI components are application specific and depend +on the underlying transport protocol. + +If this is a TCP/IP based connection and you only want the remote IP, you may +use something like this: + +```php +$address = $connection->getRemoteAddress(); +$ip = trim(parse_url($address, PHP_URL_HOST), '[]'); +echo 'Connection with ' . $ip . PHP_EOL; +``` + +#### getLocalAddress() + +The `getLocalAddress(): ?string` method returns the full local address +(URI) where this connection has been established with. + +```php +$address = $connection->getLocalAddress(); +echo 'Connection with ' . $address . PHP_EOL; +``` + +If the local address can not be determined or is unknown at this time (such as +after the connection has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full address (URI) as a string value, such +as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, +`unix://example.sock` or `unix:///path/to/example.sock`. +Note that individual URI components are application specific and depend +on the underlying transport protocol. + +This method complements the [`getRemoteAddress()`](#getremoteaddress) method, +so they should not be confused. + +If your `TcpServer` instance is listening on multiple interfaces (e.g. using +the address `0.0.0.0`), you can use this method to find out which interface +actually accepted this connection (such as a public or local interface). + +If your system has multiple interfaces (e.g. a WAN and a LAN interface), +you can use this method to find out which interface was actually +used for this connection. + +## Server usage + +### ServerInterface + +The `ServerInterface` is responsible for providing an interface for accepting +incoming streaming connections, such as a normal TCP/IP connection. + +Most higher-level components (such as a HTTP server) accept an instance +implementing this interface to accept incoming streaming connections. +This is usually done via dependency injection, so it's fairly simple to actually +swap this implementation against any other implementation of this interface. +This means that you SHOULD typehint against this interface instead of a concrete +implementation of this interface. + +Besides defining a few methods, this interface also implements the +[`EventEmitterInterface`](https://github.com/igorw/evenement) +which allows you to react to certain events. + +#### connection event + +The `connection` event will be emitted whenever a new connection has been +established, i.e. a new client connects to this server socket: + +```php +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo 'new connection' . PHP_EOL; +}); +``` + +See also the [`ConnectionInterface`](#connectioninterface) for more details +about handling the incoming connection. + +#### error event + +The `error` event will be emitted whenever there's an error accepting a new +connection from a client. + +```php +$socket->on('error', function (Exception $e) { + echo 'error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +Note that this is not a fatal error event, i.e. the server keeps listening for +new connections even after this event. + +#### getAddress() + +The `getAddress(): ?string` method can be used to +return the full address (URI) this server is currently listening on. + +```php +$address = $socket->getAddress(); +echo 'Server listening on ' . $address . PHP_EOL; +``` + +If the address can not be determined or is unknown at this time (such as +after the socket has been closed), it MAY return a `NULL` value instead. + +Otherwise, it will return the full address (URI) as a string value, such +as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443` +`unix://example.sock` or `unix:///path/to/example.sock`. +Note that individual URI components are application specific and depend +on the underlying transport protocol. + +If this is a TCP/IP based server and you only want the local port, you may +use something like this: + +```php +$address = $socket->getAddress(); +$port = parse_url($address, PHP_URL_PORT); +echo 'Server listening on port ' . $port . PHP_EOL; +``` + +#### pause() + +The `pause(): void` method can be used to +pause accepting new incoming connections. + +Removes the socket resource from the EventLoop and thus stop accepting +new connections. Note that the listening socket stays active and is not +closed. + +This means that new incoming connections will stay pending in the +operating system backlog until its configurable backlog is filled. +Once the backlog is filled, the operating system may reject further +incoming connections until the backlog is drained again by resuming +to accept new connections. + +Once the server is paused, no futher `connection` events SHOULD +be emitted. + +```php +$socket->pause(); + +$socket->on('connection', assertShouldNeverCalled()); +``` + +This method is advisory-only, though generally not recommended, the +server MAY continue emitting `connection` events. + +Unless otherwise noted, a successfully opened server SHOULD NOT start +in paused state. + +You can continue processing events by calling `resume()` again. + +Note that both methods can be called any number of times, in particular +calling `pause()` more than once SHOULD NOT have any effect. +Similarly, calling this after `close()` is a NO-OP. + +#### resume() + +The `resume(): void` method can be used to +resume accepting new incoming connections. + +Re-attach the socket resource to the EventLoop after a previous `pause()`. + +```php +$socket->pause(); + +Loop::addTimer(1.0, function () use ($socket) { + $socket->resume(); +}); +``` + +Note that both methods can be called any number of times, in particular +calling `resume()` without a prior `pause()` SHOULD NOT have any effect. +Similarly, calling this after `close()` is a NO-OP. + +#### close() + +The `close(): void` method can be used to +shut down this listening socket. + +This will stop listening for new incoming connections on this socket. + +```php +echo 'Shutting down server socket' . PHP_EOL; +$socket->close(); +``` + +Calling this method more than once on the same instance is a NO-OP. + +### SocketServer + + + +The `SocketServer` class is the main class in this package that implements the +[`ServerInterface`](#serverinterface) and allows you to accept incoming +streaming connections, such as plaintext TCP/IP or secure TLS connection streams. + +In order to accept plaintext TCP/IP connections, you can simply pass a host +and port combination like this: + +```php +$socket = new React\Socket\SocketServer('127.0.0.1:8080'); +``` + +Listening on the localhost address `127.0.0.1` means it will not be reachable from +outside of this system. +In order to change the host the socket is listening on, you can provide an IP +address of an interface or use the special `0.0.0.0` address to listen on all +interfaces: + +```php +$socket = new React\Socket\SocketServer('0.0.0.0:8080'); +``` + +If you want to listen on an IPv6 address, you MUST enclose the host in square +brackets: + +```php +$socket = new React\Socket\SocketServer('[::1]:8080'); +``` + +In order to use a random port assignment, you can use the port `0`: + +```php +$socket = new React\Socket\SocketServer('127.0.0.1:0'); +$address = $socket->getAddress(); +``` + +To listen on a Unix domain socket (UDS) path, you MUST prefix the URI with the +`unix://` scheme: + +```php +$socket = new React\Socket\SocketServer('unix:///tmp/server.sock'); +``` + +In order to listen on an existing file descriptor (FD) number, you MUST prefix +the URI with `php://fd/` like this: + +```php +$socket = new React\Socket\SocketServer('php://fd/3'); +``` + +If the given URI is invalid, does not contain a port, any other scheme or if it +contains a hostname, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException due to missing port +$socket = new React\Socket\SocketServer('127.0.0.1'); +``` + +If the given URI appears to be valid, but listening on it fails (such as if port +is already in use or port below 1024 may require root access etc.), it will +throw a `RuntimeException`: + +```php +$first = new React\Socket\SocketServer('127.0.0.1:8080'); + +// throws RuntimeException because port is already in use +$second = new React\Socket\SocketServer('127.0.0.1:8080'); +``` + +> Note that these error conditions may vary depending on your system and/or + configuration. + See the exception message and code for more details about the actual error + condition. + +Optionally, you can specify [TCP socket context options](https://www.php.net/manual/en/context.socket.php) +for the underlying stream socket resource like this: + +```php +$socket = new React\Socket\SocketServer('[::1]:8080', array( + 'tcp' => array( + 'backlog' => 200, + 'so_reuseport' => true, + 'ipv6_v6only' => true + ) +)); +``` + +> Note that available [socket context options](https://www.php.net/manual/en/context.socket.php), + their defaults and effects of changing these may vary depending on your system + and/or PHP version. + Passing unknown context options has no effect. + The `backlog` context option defaults to `511` unless given explicitly. + +You can start a secure TLS (formerly known as SSL) server by simply prepending +the `tls://` URI scheme. +Internally, it will wait for plaintext TCP/IP connections and then performs a +TLS handshake for each connection. +It thus requires valid [TLS context options](https://www.php.net/manual/en/context.ssl.php), +which in its most basic form may look something like this if you're using a +PEM encoded certificate file: + +```php +$socket = new React\Socket\SocketServer('tls://127.0.0.1:8080', array( + 'tls' => array( + 'local_cert' => 'server.pem' + ) +)); +``` + +> Note that the certificate file will not be loaded on instantiation but when an + incoming connection initializes its TLS context. + This implies that any invalid certificate file paths or contents will only cause + an `error` event at a later time. + +If your private key is encrypted with a passphrase, you have to specify it +like this: + +```php +$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array( + 'tls' => array( + 'local_cert' => 'server.pem', + 'passphrase' => 'secret' + ) +)); +``` + +By default, this server supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$socket = new React\Socket\SocketServer('tls://127.0.0.1:8000', array( + 'tls' => array( + 'local_cert' => 'server.pem', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER + ) +)); +``` + +> Note that available [TLS context options](https://www.php.net/manual/en/context.ssl.php), + their defaults and effects of changing these may vary depending on your system + and/or PHP version. + The outer context array allows you to also use `tcp` (and possibly more) + context options at the same time. + Passing unknown context options has no effect. + If you do not use the `tls://` scheme, then passing `tls` context options + has no effect. + +Whenever a client connects, it will emit a `connection` event with a connection +instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$socket->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +> Note that the `SocketServer` class is a concrete implementation for TCP/IP sockets. + If you want to typehint in your higher-level protocol implementation, you SHOULD + use the generic [`ServerInterface`](#serverinterface) instead. + +> Changelog v1.9.0: This class has been added with an improved constructor signature + as a replacement for the previous `Server` class in order to avoid any ambiguities. + The previous name has been deprecated and should not be used anymore. + +### Advanced server usage + +#### TcpServer + +The `TcpServer` class implements the [`ServerInterface`](#serverinterface) and +is responsible for accepting plaintext TCP/IP connections. + +```php +$server = new React\Socket\TcpServer(8080); +``` + +As above, the `$uri` parameter can consist of only a port, in which case the +server will default to listening on the localhost address `127.0.0.1`, +which means it will not be reachable from outside of this system. + +In order to use a random port assignment, you can use the port `0`: + +```php +$server = new React\Socket\TcpServer(0); +$address = $server->getAddress(); +``` + +In order to change the host the socket is listening on, you can provide an IP +address through the first parameter provided to the constructor, optionally +preceded by the `tcp://` scheme: + +```php +$server = new React\Socket\TcpServer('192.168.0.1:8080'); +``` + +If you want to listen on an IPv6 address, you MUST enclose the host in square +brackets: + +```php +$server = new React\Socket\TcpServer('[::1]:8080'); +``` + +If the given URI is invalid, does not contain a port, any other scheme or if it +contains a hostname, it will throw an `InvalidArgumentException`: + +```php +// throws InvalidArgumentException due to missing port +$server = new React\Socket\TcpServer('127.0.0.1'); +``` + +If the given URI appears to be valid, but listening on it fails (such as if port +is already in use or port below 1024 may require root access etc.), it will +throw a `RuntimeException`: + +```php +$first = new React\Socket\TcpServer(8080); + +// throws RuntimeException because port is already in use +$second = new React\Socket\TcpServer(8080); +``` + +> Note that these error conditions may vary depending on your system and/or +configuration. +See the exception message and code for more details about the actual error +condition. + +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. + +Optionally, you can specify [socket context options](https://www.php.net/manual/en/context.socket.php) +for the underlying stream socket resource like this: + +```php +$server = new React\Socket\TcpServer('[::1]:8080', null, array( + 'backlog' => 200, + 'so_reuseport' => true, + 'ipv6_v6only' => true +)); +``` + +> Note that available [socket context options](https://www.php.net/manual/en/context.socket.php), +their defaults and effects of changing these may vary depending on your system +and/or PHP version. +Passing unknown context options has no effect. +The `backlog` context option defaults to `511` unless given explicitly. + +Whenever a client connects, it will emit a `connection` event with a connection +instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +#### SecureServer + +The `SecureServer` class implements the [`ServerInterface`](#serverinterface) +and is responsible for providing a secure TLS (formerly known as SSL) server. + +It does so by wrapping a [`TcpServer`](#tcpserver) instance which waits for plaintext +TCP/IP connections and then performs a TLS handshake for each connection. +It thus requires valid [TLS context options](https://www.php.net/manual/en/context.ssl.php), +which in its most basic form may look something like this if you're using a +PEM encoded certificate file: + +```php +$server = new React\Socket\TcpServer(8000); +$server = new React\Socket\SecureServer($server, null, array( + 'local_cert' => 'server.pem' +)); +``` + +> Note that the certificate file will not be loaded on instantiation but when an +incoming connection initializes its TLS context. +This implies that any invalid certificate file paths or contents will only cause +an `error` event at a later time. + +If your private key is encrypted with a passphrase, you have to specify it +like this: + +```php +$server = new React\Socket\TcpServer(8000); +$server = new React\Socket\SecureServer($server, null, array( + 'local_cert' => 'server.pem', + 'passphrase' => 'secret' +)); +``` + +By default, this server supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$server = new React\Socket\TcpServer(8000); +$server = new React\Socket\SecureServer($server, null, array( + 'local_cert' => 'server.pem', + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER +)); +``` + +> Note that available [TLS context options](https://www.php.net/manual/en/context.ssl.php), +their defaults and effects of changing these may vary depending on your system +and/or PHP version. +Passing unknown context options has no effect. + +Whenever a client completes the TLS handshake, it will emit a `connection` event +with a connection instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +Whenever a client fails to perform a successful TLS handshake, it will emit an +`error` event and then close the underlying TCP/IP connection: + +```php +$server->on('error', function (Exception $e) { + echo 'Error' . $e->getMessage() . PHP_EOL; +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +Note that the `SecureServer` class is a concrete implementation for TLS sockets. +If you want to typehint in your higher-level protocol implementation, you SHOULD +use the generic [`ServerInterface`](#serverinterface) instead. + +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +> Advanced usage: Despite allowing any `ServerInterface` as first parameter, +you SHOULD pass a `TcpServer` instance as first parameter, unless you +know what you're doing. +Internally, the `SecureServer` has to set the required TLS context options on +the underlying stream resources. +These resources are not exposed through any of the interfaces defined in this +package, but only through the internal `Connection` class. +The `TcpServer` class is guaranteed to emit connections that implement +the `ConnectionInterface` and uses the internal `Connection` class in order to +expose these underlying resources. +If you use a custom `ServerInterface` and its `connection` event does not +meet this requirement, the `SecureServer` will emit an `error` event and +then close the underlying connection. + +#### UnixServer + +The `UnixServer` class implements the [`ServerInterface`](#serverinterface) and +is responsible for accepting connections on Unix domain sockets (UDS). + +```php +$server = new React\Socket\UnixServer('/tmp/server.sock'); +``` + +As above, the `$uri` parameter can consist of only a socket path or socket path +prefixed by the `unix://` scheme. + +If the given URI appears to be valid, but listening on it fails (such as if the +socket is already in use or the file not accessible etc.), it will throw a +`RuntimeException`: + +```php +$first = new React\Socket\UnixServer('/tmp/same.sock'); + +// throws RuntimeException because socket is already in use +$second = new React\Socket\UnixServer('/tmp/same.sock'); +``` + +> Note that these error conditions may vary depending on your system and/or + configuration. + In particular, Zend PHP does only report "Unknown error" when the UDS path + already exists and can not be bound. You may want to check `is_file()` on the + given UDS path to report a more user-friendly error message in this case. + See the exception message and code for more details about the actual error + condition. + +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. + +Whenever a client connects, it will emit a `connection` event with a connection +instance implementing [`ConnectionInterface`](#connectioninterface): + +```php +$server->on('connection', function (React\Socket\ConnectionInterface $connection) { + echo 'New connection' . PHP_EOL; + + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [`ServerInterface`](#serverinterface) for more details. + +#### LimitingServer + +The `LimitingServer` decorator wraps a given `ServerInterface` and is responsible +for limiting and keeping track of open connections to this server instance. + +Whenever the underlying server emits a `connection` event, it will check its +limits and then either + - keep track of this connection by adding it to the list of + open connections and then forward the `connection` event + - or reject (close) the connection when its limits are exceeded and will + forward an `error` event instead. + +Whenever a connection closes, it will remove this connection from the list of +open connections. + +```php +$server = new React\Socket\LimitingServer($server, 100); +$server->on('connection', function (React\Socket\ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +See also the [second example](examples) for more details. + +You have to pass a maximum number of open connections to ensure +the server will automatically reject (close) connections once this limit +is exceeded. In this case, it will emit an `error` event to inform about +this and no `connection` event will be emitted. + +```php +$server = new React\Socket\LimitingServer($server, 100); +$server->on('connection', function (React\Socket\ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +You MAY pass a `null` limit in order to put no limit on the number of +open connections and keep accepting new connection until you run out of +operating system resources (such as open file handles). This may be +useful if you do not want to take care of applying a limit but still want +to use the `getConnections()` method. + +You can optionally configure the server to pause accepting new +connections once the connection limit is reached. In this case, it will +pause the underlying server and no longer process any new connections at +all, thus also no longer closing any excessive connections. +The underlying operating system is responsible for keeping a backlog of +pending connections until its limit is reached, at which point it will +start rejecting further connections. +Once the server is below the connection limit, it will continue consuming +connections from the backlog and will process any outstanding data on +each connection. +This mode may be useful for some protocols that are designed to wait for +a response message (such as HTTP), but may be less useful for other +protocols that demand immediate responses (such as a "welcome" message in +an interactive chat). + +```php +$server = new React\Socket\LimitingServer($server, 100, true); +$server->on('connection', function (React\Socket\ConnectionInterface $connection) { + $connection->write('hello there!' . PHP_EOL); + … +}); +``` + +##### getConnections() + +The `getConnections(): ConnectionInterface[]` method can be used to +return an array with all currently active connections. + +```php +foreach ($server->getConnection() as $connection) { + $connection->write('Hi!'); +} +``` + +## Client usage + +### ConnectorInterface + +The `ConnectorInterface` is responsible for providing an interface for +establishing streaming connections, such as a normal TCP/IP connection. + +This is the main interface defined in this package and it is used throughout +React's vast ecosystem. + +Most higher-level components (such as HTTP, database or other networking +service clients) accept an instance implementing this interface to create their +TCP/IP connection to the underlying networking service. +This is usually done via dependency injection, so it's fairly simple to actually +swap this implementation against any other implementation of this interface. + +The interface only offers a single method: + +#### connect() + +The `connect(string $uri): PromiseInterface` method can be used to +create a streaming connection to the given remote address. + +It returns a [Promise](https://github.com/reactphp/promise) which either +fulfills with a stream implementing [`ConnectionInterface`](#connectioninterface) +on success or rejects with an `Exception` if the connection is not successful: + +```php +$connector->connect('google.com:443')->then( + function (React\Socket\ConnectionInterface $connection) { + // connection successfully established + }, + function (Exception $error) { + // failed to connect due to $error + } +); +``` + +See also [`ConnectionInterface`](#connectioninterface) for more details. + +The returned Promise MUST be implemented in such a way that it can be +cancelled when it is still pending. Cancelling a pending promise MUST +reject its value with an `Exception`. It SHOULD clean up any underlying +resources and references as applicable: + +```php +$promise = $connector->connect($uri); + +$promise->cancel(); +``` + +### Connector + +The `Connector` class is the main class in this package that implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create streaming connections. + +You can use this connector to create any kind of streaming connections, such +as plaintext TCP/IP, secure TLS or local Unix connection streams. + +It binds to the main event loop and can be used like this: + +```php +$connector = new React\Socket\Connector(); + +$connector->connect($uri)->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +In order to create a plaintext TCP/IP connection, you can simply pass a host +and port combination like this: + +```php +$connector->connect('www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> If you do no specify a URI scheme in the destination URI, it will assume + `tcp://` as a default and establish a plaintext TCP/IP connection. + Note that TCP/IP connections require a host and port part in the destination + URI like above, all other URI components are optional. + +In order to create a secure TLS connection, you can use the `tls://` URI scheme +like this: + +```php +$connector->connect('tls://www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +In order to create a local Unix domain socket connection, you can use the +`unix://` URI scheme like this: + +```php +$connector->connect('unix:///tmp/demo.sock')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> The [`getRemoteAddress()`](#getremoteaddress) method will return the target + Unix domain socket (UDS) path as given to the `connect()` method, including + the `unix://` scheme, for example `unix:///tmp/demo.sock`. + The [`getLocalAddress()`](#getlocaladdress) method will most likely return a + `null` value as this value is not applicable to UDS connections here. + +Under the hood, the `Connector` is implemented as a *higher-level facade* +for the lower-level connectors implemented in this package. This means it +also shares all of their features and implementation details. +If you want to typehint in your higher-level protocol implementation, you SHOULD +use the generic [`ConnectorInterface`](#connectorinterface) instead. + +As of `v1.4.0`, the `Connector` class defaults to using the +[happy eyeballs algorithm](https://en.wikipedia.org/wiki/Happy_Eyeballs) to +automatically connect over IPv4 or IPv6 when a hostname is given. +This automatically attempts to connect using both IPv4 and IPv6 at the same time +(preferring IPv6), thus avoiding the usual problems faced by users with imperfect +IPv6 connections or setups. +If you want to revert to the old behavior of only doing an IPv4 lookup and +only attempt a single IPv4 connection, you can set up the `Connector` like this: + +```php +$connector = new React\Socket\Connector(array( + 'happy_eyeballs' => false +)); +``` + +Similarly, you can also affect the default DNS behavior as follows. +The `Connector` class will try to detect your system DNS settings (and uses +Google's public DNS server `8.8.8.8` as a fallback if unable to determine your +system settings) to resolve all public hostnames into underlying IP addresses by +default. +If you explicitly want to use a custom DNS server (such as a local DNS relay or +a company wide DNS server), you can set up the `Connector` like this: + +```php +$connector = new React\Socket\Connector(array( + 'dns' => '127.0.1.1' +)); + +$connector->connect('localhost:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +If you do not want to use a DNS resolver at all and want to connect to IP +addresses only, you can also set up your `Connector` like this: + +```php +$connector = new React\Socket\Connector(array( + 'dns' => false +)); + +$connector->connect('127.0.0.1:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +Advanced: If you need a custom DNS `React\Dns\Resolver\ResolverInterface` instance, you +can also set up your `Connector` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('127.0.1.1'); + +$connector = new React\Socket\Connector(array( + 'dns' => $resolver +)); + +$connector->connect('localhost:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +By default, the `tcp://` and `tls://` URI schemes will use timeout value that +respects your `default_socket_timeout` ini setting (which defaults to 60s). +If you want a custom timeout value, you can simply pass this like this: + +```php +$connector = new React\Socket\Connector(array( + 'timeout' => 10.0 +)); +``` + +Similarly, if you do not want to apply a timeout at all and let the operating +system handle this, you can pass a boolean flag like this: + +```php +$connector = new React\Socket\Connector(array( + 'timeout' => false +)); +``` + +By default, the `Connector` supports the `tcp://`, `tls://` and `unix://` +URI schemes. If you want to explicitly prohibit any of these, you can simply +pass boolean flags like this: + +```php +// only allow secure TLS connections +$connector = new React\Socket\Connector(array( + 'tcp' => false, + 'tls' => true, + 'unix' => false, +)); + +$connector->connect('tls://google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +The `tcp://` and `tls://` also accept additional context options passed to +the underlying connectors. +If you want to explicitly pass additional context options, you can simply +pass arrays of context options like this: + +```php +// allow insecure TLS connections +$connector = new React\Socket\Connector(array( + 'tcp' => array( + 'bindto' => '192.168.0.1:0' + ), + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ), +)); + +$connector->connect('tls://localhost:443')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +By default, this connector supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$connector = new React\Socket\Connector(array( + 'tls' => array( + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + ) +)); +``` + +> For more details about context options, please refer to the PHP documentation + about [socket context options](https://www.php.net/manual/en/context.socket.php) + and [SSL context options](https://www.php.net/manual/en/context.ssl.php). + +Advanced: By default, the `Connector` supports the `tcp://`, `tls://` and +`unix://` URI schemes. +For this, it sets up the required connector classes automatically. +If you want to explicitly pass custom connectors for any of these, you can simply +pass an instance implementing the `ConnectorInterface` like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$resolver = $dnsResolverFactory->createCached('127.0.1.1'); +$tcp = new React\Socket\HappyEyeBallsConnector(null, new React\Socket\TcpConnector(), $resolver); + +$tls = new React\Socket\SecureConnector($tcp); + +$unix = new React\Socket\UnixConnector(); + +$connector = new React\Socket\Connector(array( + 'tcp' => $tcp, + 'tls' => $tls, + 'unix' => $unix, + + 'dns' => false, + 'timeout' => false, +)); + +$connector->connect('google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +> Internally, the `tcp://` connector will always be wrapped by the DNS resolver, + unless you disable DNS like in the above example. In this case, the `tcp://` + connector receives the actual hostname instead of only the resolved IP address + and is thus responsible for performing the lookup. + Internally, the automatically created `tls://` connector will always wrap the + underlying `tcp://` connector for establishing the underlying plaintext + TCP/IP connection before enabling secure TLS mode. If you want to use a custom + underlying `tcp://` connector for secure TLS connections only, you may + explicitly pass a `tls://` connector like above instead. + Internally, the `tcp://` and `tls://` connectors will always be wrapped by + `TimeoutConnector`, unless you disable timeouts like in the above example. + +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. + +> Changelog v1.9.0: The constructur signature has been updated to take the +> optional `$context` as the first parameter and the optional `$loop` as a second +> argument. The previous signature has been deprecated and should not be used anymore. +> +> ```php +> // constructor signature as of v1.9.0 +> $connector = new React\Socket\Connector(array $context = [], ?LoopInterface $loop = null); +> +> // legacy constructor signature before v1.9.0 +> $connector = new React\Socket\Connector(?LoopInterface $loop = null, array $context = []); +> ``` + +### Advanced client usage + +#### TcpConnector + +The `TcpConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any IP-port-combination: + +```php +$tcpConnector = new React\Socket\TcpConnector(); + +$tcpConnector->connect('127.0.0.1:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $tcpConnector->connect('127.0.0.1:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will close the underlying socket +resource, thus cancelling the pending TCP/IP connection, and reject the +resulting promise. + +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. + +You can optionally pass additional +[socket context options](https://www.php.net/manual/en/context.socket.php) +to the constructor like this: + +```php +$tcpConnector = new React\Socket\TcpConnector(null, array( + 'bindto' => '192.168.0.1:0' +)); +``` + +Note that this class only allows you to connect to IP-port-combinations. +If the given URI is invalid, does not contain a valid IP address and port +or contains any other scheme, it will reject with an +`InvalidArgumentException`: + +If the given URI appears to be valid, but connecting to it fails (such as if +the remote host rejects the connection etc.), it will reject with a +`RuntimeException`. + +If you want to connect to hostname-port-combinations, see also the following chapter. + +> Advanced usage: Internally, the `TcpConnector` allocates an empty *context* +resource for each stream resource. +If the destination URI contains a `hostname` query parameter, its value will +be used to set up the TLS peer name. +This is used by the `SecureConnector` and `DnsConnector` to verify the peer +name and can also be used if you want a custom TLS peer name. + +#### HappyEyeBallsConnector + +The `HappyEyeBallsConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any hostname-port-combination. Internally it implements the +happy eyeballs algorithm from [`RFC6555`](https://tools.ietf.org/html/rfc6555) and +[`RFC8305`](https://tools.ietf.org/html/rfc8305) to support IPv6 and IPv4 hostnames. + +It does so by decorating a given `TcpConnector` instance so that it first +looks up the given domain name via DNS (if applicable) and then establishes the +underlying TCP/IP connection to the resolved target IP address. + +Make sure to set up your DNS resolver and underlying TCP connector like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$dns = $dnsResolverFactory->createCached('8.8.8.8'); + +$dnsConnector = new React\Socket\HappyEyeBallsConnector(null, $tcpConnector, $dns); + +$dnsConnector->connect('www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $dnsConnector->connect('www.google.com:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying DNS lookups +and/or the underlying TCP/IP connection(s) and reject the resulting promise. + +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +> Advanced usage: Internally, the `HappyEyeBallsConnector` relies on a `Resolver` to +look up the IP addresses for the given hostname. +It will then replace the hostname in the destination URI with this IP's and +append a `hostname` query parameter and pass this updated URI to the underlying +connector. +The Happy Eye Balls algorithm describes looking the IPv6 and IPv4 address for +the given hostname so this connector sends out two DNS lookups for the A and +AAAA records. It then uses all IP addresses (both v6 and v4) and tries to +connect to all of them with a 50ms interval in between. Alterating between IPv6 +and IPv4 addresses. When a connection is established all the other DNS lookups +and connection attempts are cancelled. + +#### DnsConnector + +The `DnsConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create plaintext +TCP/IP connections to any hostname-port-combination. + +It does so by decorating a given `TcpConnector` instance so that it first +looks up the given domain name via DNS (if applicable) and then establishes the +underlying TCP/IP connection to the resolved target IP address. + +Make sure to set up your DNS resolver and underlying TCP connector like this: + +```php +$dnsResolverFactory = new React\Dns\Resolver\Factory(); +$dns = $dnsResolverFactory->createCached('8.8.8.8'); + +$dnsConnector = new React\Socket\DnsConnector($tcpConnector, $dns); + +$dnsConnector->connect('www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write('...'); + $connection->end(); +}); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $dnsConnector->connect('www.google.com:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying DNS lookup +and/or the underlying TCP/IP connection and reject the resulting promise. + +> Advanced usage: Internally, the `DnsConnector` relies on a `React\Dns\Resolver\ResolverInterface` +to look up the IP address for the given hostname. +It will then replace the hostname in the destination URI with this IP and +append a `hostname` query parameter and pass this updated URI to the underlying +connector. +The underlying connector is thus responsible for creating a connection to the +target IP address, while this query parameter can be used to check the original +hostname and is used by the `TcpConnector` to set up the TLS peer name. +If a `hostname` is given explicitly, this query parameter will not be modified, +which can be useful if you want a custom TLS peer name. + +#### SecureConnector + +The `SecureConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to create secure +TLS (formerly known as SSL) connections to any hostname-port-combination. + +It does so by decorating a given `DnsConnector` instance so that it first +creates a plaintext TCP/IP connection and then enables TLS encryption on this +stream. + +```php +$secureConnector = new React\Socket\SecureConnector($dnsConnector); + +$secureConnector->connect('www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\nHost: www.google.com\r\n\r\n"); + ... +}); +``` + +See also the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $secureConnector->connect('www.google.com:443'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying TCP/IP +connection and/or the SSL/TLS negotiation and reject the resulting promise. + +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. + +You can optionally pass additional +[SSL context options](https://www.php.net/manual/en/context.ssl.php) +to the constructor like this: + +```php +$secureConnector = new React\Socket\SecureConnector($dnsConnector, null, array( + 'verify_peer' => false, + 'verify_peer_name' => false +)); +``` + +By default, this connector supports TLSv1.0+ and excludes support for legacy +SSLv2/SSLv3. As of PHP 5.6+ you can also explicitly choose the TLS version you +want to negotiate with the remote side: + +```php +$secureConnector = new React\Socket\SecureConnector($dnsConnector, null, array( + 'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT +)); +``` + +> Advanced usage: Internally, the `SecureConnector` relies on setting up the +required *context options* on the underlying stream resource. +It should therefor be used with a `TcpConnector` somewhere in the connector +stack so that it can allocate an empty *context* resource for each stream +resource and verify the peer name. +Failing to do so may result in a TLS peer name mismatch error or some hard to +trace race conditions, because all stream resources will use a single, shared +*default context* resource otherwise. + +#### TimeoutConnector + +The `TimeoutConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to add timeout +handling to any existing connector instance. + +It does so by decorating any given [`ConnectorInterface`](#connectorinterface) +instance and starting a timer that will automatically reject and abort any +underlying connection attempt if it takes too long. + +```php +$timeoutConnector = new React\Socket\TimeoutConnector($connector, 3.0); + +$timeoutConnector->connect('google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + // connection succeeded within 3.0 seconds +}); +``` + +See also any of the [examples](examples). + +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. + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $timeoutConnector->connect('google.com:80'); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying connection +attempt, abort the timer and reject the resulting promise. + +#### UnixConnector + +The `UnixConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and allows you to connect to +Unix domain socket (UDS) paths like this: + +```php +$connector = new React\Socket\UnixConnector(); + +$connector->connect('/tmp/demo.sock')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("HELLO\n"); +}); +``` + +Connecting to Unix domain sockets is an atomic operation, i.e. its promise will +settle (either resolve or reject) immediately. +As such, calling `cancel()` on the resulting promise has no effect. + +> The [`getRemoteAddress()`](#getremoteaddress) method will return the target + Unix domain socket (UDS) path as given to the `connect()` method, prepended + with the `unix://` scheme, for example `unix:///tmp/demo.sock`. + The [`getLocalAddress()`](#getlocaladdress) method will most likely return a + `null` value as this value is not applicable to UDS connections here. + +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. + +#### FixedUriConnector + +The `FixedUriConnector` class implements the +[`ConnectorInterface`](#connectorinterface) and decorates an existing Connector +to always use a fixed, preconfigured URI. + +This can be useful for consumers that do not support certain URIs, such as +when you want to explicitly connect to a Unix domain socket (UDS) path +instead of connecting to a default address assumed by an higher-level API: + +```php +$connector = new React\Socket\FixedUriConnector( + 'unix:///var/run/docker.sock', + new React\Socket\UnixConnector() +); + +// destination will be ignored, actually connects to Unix domain socket +$promise = $connector->connect('localhost:80'); +``` + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org/). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](https://semver.org/). +This will install the latest supported version: + +```bash +composer require react/socket:^1.16 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and HHVM. +It's *highly recommended to use the latest supported PHP version* for this project, +partly due to its vast performance improvements and partly because legacy PHP +versions require several workarounds as described below. + +Secure TLS connections received some major upgrades starting with PHP 5.6, with +the defaults now being more secure, while older versions required explicit +context options. +This library does not take responsibility over these context options, so it's +up to consumers of this library to take care of setting appropriate context +options as described above. + +PHP < 7.3.3 (and PHP < 7.2.15) suffers from a bug where feof() might +block with 100% CPU usage on fragmented TLS records. +We try to work around this by always consuming the complete receive +buffer at once to avoid stale data in TLS buffers. This is known to +work around high CPU usage for well-behaving peers, but this may +cause very large data chunks for high throughput scenarios. The buggy +behavior can still be triggered due to network I/O buffers or +malicious peers on affected versions, upgrading is highly recommended. + +PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big +chunks of data over TLS streams at once. +We try to work around this by limiting the write chunk size to 8192 +bytes for older PHP versions only. +This is only a work-around and has a noticable performance penalty on +affected versions. + +This project also supports running on HHVM. +Note that really old HHVM < 3.8 does not support secure TLS connections, as it +lacks the required `stream_socket_enable_crypto()` function. +As such, trying to create a secure TLS connections on affected versions will +return a rejected promise instead. +This issue is also covered by our test suite, which will skip related tests +on affected versions. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +The test suite also contains a number of functional integration tests that rely +on a stable internet connection. +If you do not want to run these, they can simply be skipped like this: + +```bash +vendor/bin/phpunit --exclude-group internet +``` + +## License + +MIT, see [LICENSE file](LICENSE). diff --git a/vendor/react/socket/composer.json b/vendor/react/socket/composer.json new file mode 100644 index 0000000..b1e1d25 --- /dev/null +++ b/vendor/react/socket/composer.json @@ -0,0 +1,52 @@ +{ + "name": "react/socket", + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": ["async", "socket", "stream", "connection", "ReactPHP"], + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "homepage": "https://clue.engineering/", + "email": "christian@clue.engineering" + }, + { + "name": "Cees-Jan Kiewiet", + "homepage": "https://wyrihaximus.net/", + "email": "reactphp@ceesjankiewiet.nl" + }, + { + "name": "Jan Sorgalla", + "homepage": "https://sorgalla.com/", + "email": "jsorgalla@gmail.com" + }, + { + "name": "Chris Boden", + "homepage": "https://cboden.dev/", + "email": "cboden@gmail.com" + } + ], + "require": { + "php": ">=5.3.0", + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "React\\Tests\\Socket\\": "tests/" + } + } +} diff --git a/vendor/react/socket/src/Connection.php b/vendor/react/socket/src/Connection.php new file mode 100644 index 0000000..65ae26b --- /dev/null +++ b/vendor/react/socket/src/Connection.php @@ -0,0 +1,183 @@ += 70300 && \PHP_VERSION_ID < 70303); + + // PHP < 7.1.4 (and PHP < 7.0.18) suffers from a bug when writing big + // chunks of data over TLS streams at once. + // We try to work around this by limiting the write chunk size to 8192 + // bytes for older PHP versions only. + // This is only a work-around and has a noticable performance penalty on + // affected versions. Please update your PHP version. + // This applies to all streams because TLS may be enabled later on. + // See https://github.com/reactphp/socket/issues/105 + $limitWriteChunks = (\PHP_VERSION_ID < 70018 || (\PHP_VERSION_ID >= 70100 && \PHP_VERSION_ID < 70104)); + + $this->input = new DuplexResourceStream( + $resource, + $loop, + $clearCompleteBuffer ? -1 : null, + new WritableResourceStream($resource, $loop, null, $limitWriteChunks ? 8192 : null) + ); + + $this->stream = $resource; + + Util::forwardEvents($this->input, $this, array('data', 'end', 'error', 'close', 'pipe', 'drain')); + + $this->input->on('close', array($this, 'close')); + } + + public function isReadable() + { + return $this->input->isReadable(); + } + + public function isWritable() + { + return $this->input->isWritable(); + } + + public function pause() + { + $this->input->pause(); + } + + public function resume() + { + $this->input->resume(); + } + + public function pipe(WritableStreamInterface $dest, array $options = array()) + { + return $this->input->pipe($dest, $options); + } + + public function write($data) + { + return $this->input->write($data); + } + + public function end($data = null) + { + $this->input->end($data); + } + + public function close() + { + $this->input->close(); + $this->handleClose(); + $this->removeAllListeners(); + } + + public function handleClose() + { + if (!\is_resource($this->stream)) { + return; + } + + // Try to cleanly shut down socket and ignore any errors in case other + // side already closed. Underlying Stream implementation will take care + // of closing stream resource, so we otherwise keep this open here. + @\stream_socket_shutdown($this->stream, \STREAM_SHUT_RDWR); + } + + public function getRemoteAddress() + { + if (!\is_resource($this->stream)) { + return null; + } + + return $this->parseAddress(\stream_socket_get_name($this->stream, true)); + } + + public function getLocalAddress() + { + if (!\is_resource($this->stream)) { + return null; + } + + return $this->parseAddress(\stream_socket_get_name($this->stream, false)); + } + + private function parseAddress($address) + { + if ($address === false) { + return null; + } + + if ($this->unix) { + // remove trailing colon from address for HHVM < 3.19: https://3v4l.org/5C1lo + // note that technically ":" is a valid address, so keep this in place otherwise + if (\substr($address, -1) === ':' && \defined('HHVM_VERSION_ID') && \HHVM_VERSION_ID < 31900) { + $address = (string)\substr($address, 0, -1); // @codeCoverageIgnore + } + + // work around unknown addresses should return null value: https://3v4l.org/5C1lo and https://bugs.php.net/bug.php?id=74556 + // PHP uses "\0" string and HHVM uses empty string (colon removed above) + if ($address === '' || $address[0] === "\x00" ) { + return null; // @codeCoverageIgnore + } + + return 'unix://' . $address; + } + + // check if this is an IPv6 address which includes multiple colons but no square brackets + $pos = \strrpos($address, ':'); + if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') { + $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore + } + + return ($this->encryptionEnabled ? 'tls' : 'tcp') . '://' . $address; + } +} diff --git a/vendor/react/socket/src/ConnectionInterface.php b/vendor/react/socket/src/ConnectionInterface.php new file mode 100644 index 0000000..64613b5 --- /dev/null +++ b/vendor/react/socket/src/ConnectionInterface.php @@ -0,0 +1,119 @@ +on('data', function ($chunk) { + * echo $chunk; + * }); + * + * $connection->on('end', function () { + * echo 'ended'; + * }); + * + * $connection->on('error', function (Exception $e) { + * echo 'error: ' . $e->getMessage(); + * }); + * + * $connection->on('close', function () { + * echo 'closed'; + * }); + * + * $connection->write($data); + * $connection->end($data = null); + * $connection->close(); + * // … + * ``` + * + * For more details, see the + * [`DuplexStreamInterface`](https://github.com/reactphp/stream#duplexstreaminterface). + * + * @see DuplexStreamInterface + * @see ServerInterface + * @see ConnectorInterface + */ +interface ConnectionInterface extends DuplexStreamInterface +{ + /** + * Returns the full remote address (URI) where this connection has been established with + * + * ```php + * $address = $connection->getRemoteAddress(); + * echo 'Connection with ' . $address . PHP_EOL; + * ``` + * + * If the remote address can not be determined or is unknown at this time (such as + * after the connection has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full address (URI) as a string value, such + * as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, + * `unix://example.sock` or `unix:///path/to/example.sock`. + * Note that individual URI components are application specific and depend + * on the underlying transport protocol. + * + * If this is a TCP/IP based connection and you only want the remote IP, you may + * use something like this: + * + * ```php + * $address = $connection->getRemoteAddress(); + * $ip = trim(parse_url($address, PHP_URL_HOST), '[]'); + * echo 'Connection with ' . $ip . PHP_EOL; + * ``` + * + * @return ?string remote address (URI) or null if unknown + */ + public function getRemoteAddress(); + + /** + * Returns the full local address (full URI with scheme, IP and port) where this connection has been established with + * + * ```php + * $address = $connection->getLocalAddress(); + * echo 'Connection with ' . $address . PHP_EOL; + * ``` + * + * If the local address can not be determined or is unknown at this time (such as + * after the connection has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full address (URI) as a string value, such + * as `tcp://127.0.0.1:8080`, `tcp://[::1]:80`, `tls://127.0.0.1:443`, + * `unix://example.sock` or `unix:///path/to/example.sock`. + * Note that individual URI components are application specific and depend + * on the underlying transport protocol. + * + * This method complements the [`getRemoteAddress()`](#getremoteaddress) method, + * so they should not be confused. + * + * If your `TcpServer` instance is listening on multiple interfaces (e.g. using + * the address `0.0.0.0`), you can use this method to find out which interface + * actually accepted this connection (such as a public or local interface). + * + * If your system has multiple interfaces (e.g. a WAN and a LAN interface), + * you can use this method to find out which interface was actually + * used for this connection. + * + * @return ?string local address (URI) or null if unknown + * @see self::getRemoteAddress() + */ + public function getLocalAddress(); +} diff --git a/vendor/react/socket/src/Connector.php b/vendor/react/socket/src/Connector.php new file mode 100644 index 0000000..15faa46 --- /dev/null +++ b/vendor/react/socket/src/Connector.php @@ -0,0 +1,236 @@ + true, + 'tls' => true, + 'unix' => true, + + 'dns' => true, + 'timeout' => true, + 'happy_eyeballs' => true, + ); + + if ($context['timeout'] === true) { + $context['timeout'] = (float)\ini_get("default_socket_timeout"); + } + + if ($context['tcp'] instanceof ConnectorInterface) { + $tcp = $context['tcp']; + } else { + $tcp = new TcpConnector( + $loop, + \is_array($context['tcp']) ? $context['tcp'] : array() + ); + } + + if ($context['dns'] !== false) { + if ($context['dns'] instanceof ResolverInterface) { + $resolver = $context['dns']; + } else { + if ($context['dns'] !== true) { + $config = $context['dns']; + } else { + // try to load nameservers from system config or default to Google's public DNS + $config = DnsConfig::loadSystemConfigBlocking(); + if (!$config->nameservers) { + $config->nameservers[] = '8.8.8.8'; // @codeCoverageIgnore + } + } + + $factory = new DnsFactory(); + $resolver = $factory->createCached( + $config, + $loop + ); + } + + if ($context['happy_eyeballs'] === true) { + $tcp = new HappyEyeBallsConnector($loop, $tcp, $resolver); + } else { + $tcp = new DnsConnector($tcp, $resolver); + } + } + + if ($context['tcp'] !== false) { + $context['tcp'] = $tcp; + + if ($context['timeout'] !== false) { + $context['tcp'] = new TimeoutConnector( + $context['tcp'], + $context['timeout'], + $loop + ); + } + + $this->connectors['tcp'] = $context['tcp']; + } + + if ($context['tls'] !== false) { + if (!$context['tls'] instanceof ConnectorInterface) { + $context['tls'] = new SecureConnector( + $tcp, + $loop, + \is_array($context['tls']) ? $context['tls'] : array() + ); + } + + if ($context['timeout'] !== false) { + $context['tls'] = new TimeoutConnector( + $context['tls'], + $context['timeout'], + $loop + ); + } + + $this->connectors['tls'] = $context['tls']; + } + + if ($context['unix'] !== false) { + if (!$context['unix'] instanceof ConnectorInterface) { + $context['unix'] = new UnixConnector($loop); + } + $this->connectors['unix'] = $context['unix']; + } + } + + public function connect($uri) + { + $scheme = 'tcp'; + if (\strpos($uri, '://') !== false) { + $scheme = (string)\substr($uri, 0, \strpos($uri, '://')); + } + + if (!isset($this->connectors[$scheme])) { + return \React\Promise\reject(new \RuntimeException( + 'No connector available for URI scheme "' . $scheme . '" (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + return $this->connectors[$scheme]->connect($uri); + } + + + /** + * [internal] Builds on URI from the given URI parts and ip address with original hostname as query + * + * @param array $parts + * @param string $host + * @param string $ip + * @return string + * @internal + */ + public static function uri(array $parts, $host, $ip) + { + $uri = ''; + + // prepend original scheme if known + if (isset($parts['scheme'])) { + $uri .= $parts['scheme'] . '://'; + } + + if (\strpos($ip, ':') !== false) { + // enclose IPv6 addresses in square brackets before appending port + $uri .= '[' . $ip . ']'; + } else { + $uri .= $ip; + } + + // append original port if known + if (isset($parts['port'])) { + $uri .= ':' . $parts['port']; + } + + // append orignal path if known + if (isset($parts['path'])) { + $uri .= $parts['path']; + } + + // append original query if known + if (isset($parts['query'])) { + $uri .= '?' . $parts['query']; + } + + // append original hostname as query if resolved via DNS and if + // destination URI does not contain "hostname" query param already + $args = array(); + \parse_str(isset($parts['query']) ? $parts['query'] : '', $args); + if ($host !== $ip && !isset($args['hostname'])) { + $uri .= (isset($parts['query']) ? '&' : '?') . 'hostname=' . \rawurlencode($host); + } + + // append original fragment if known + if (isset($parts['fragment'])) { + $uri .= '#' . $parts['fragment']; + } + + return $uri; + } +} diff --git a/vendor/react/socket/src/ConnectorInterface.php b/vendor/react/socket/src/ConnectorInterface.php new file mode 100644 index 0000000..1f07b75 --- /dev/null +++ b/vendor/react/socket/src/ConnectorInterface.php @@ -0,0 +1,59 @@ +connect('google.com:443')->then( + * function (React\Socket\ConnectionInterface $connection) { + * // connection successfully established + * }, + * function (Exception $error) { + * // failed to connect due to $error + * } + * ); + * ``` + * + * The returned Promise MUST be implemented in such a way that it can be + * cancelled when it is still pending. Cancelling a pending promise MUST + * reject its value with an Exception. It SHOULD clean up any underlying + * resources and references as applicable. + * + * ```php + * $promise = $connector->connect($uri); + * + * $promise->cancel(); + * ``` + * + * @param string $uri + * @return \React\Promise\PromiseInterface + * Resolves with a `ConnectionInterface` on success or rejects with an `Exception` on error. + * @see ConnectionInterface + */ + public function connect($uri); +} diff --git a/vendor/react/socket/src/DnsConnector.php b/vendor/react/socket/src/DnsConnector.php new file mode 100644 index 0000000..d2fb2c7 --- /dev/null +++ b/vendor/react/socket/src/DnsConnector.php @@ -0,0 +1,117 @@ +connector = $connector; + $this->resolver = $resolver; + } + + public function connect($uri) + { + $original = $uri; + if (\strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + $parts = \parse_url($uri); + if (isset($parts['scheme'])) { + unset($parts['scheme']); + } + } else { + $parts = \parse_url($uri); + } + + if (!$parts || !isset($parts['host'])) { + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $original . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + $host = \trim($parts['host'], '[]'); + $connector = $this->connector; + + // skip DNS lookup / URI manipulation if this URI already contains an IP + if (@\inet_pton($host) !== false) { + return $connector->connect($original); + } + + $promise = $this->resolver->resolve($host); + $resolved = null; + + return new Promise\Promise( + function ($resolve, $reject) use (&$promise, &$resolved, $uri, $connector, $host, $parts) { + // resolve/reject with result of DNS lookup + $promise->then(function ($ip) use (&$promise, &$resolved, $uri, $connector, $host, $parts) { + $resolved = $ip; + + return $promise = $connector->connect( + Connector::uri($parts, $host, $ip) + )->then(null, function (\Exception $e) use ($uri) { + if ($e instanceof \RuntimeException) { + $message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage()); + $e = new \RuntimeException( + 'Connection to ' . $uri . ' failed: ' . $message, + $e->getCode(), + $e + ); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $r->setAccessible(true); + $trace = $r->getValue($e); + + // Exception trace arguments are not available on some PHP 7.4 installs + // @codeCoverageIgnoreStart + foreach ($trace as $ti => $one) { + if (isset($one['args'])) { + foreach ($one['args'] as $ai => $arg) { + if ($arg instanceof \Closure) { + $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($e, $trace); + } + + throw $e; + }); + }, function ($e) use ($uri, $reject) { + $reject(new \RuntimeException('Connection to ' . $uri .' failed during DNS lookup: ' . $e->getMessage(), 0, $e)); + })->then($resolve, $reject); + }, + function ($_, $reject) use (&$promise, &$resolved, $uri) { + // cancellation should reject connection attempt + // reject DNS resolution with custom reason, otherwise rely on connection cancellation below + if ($resolved === null) { + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during DNS lookup (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); + } + + // (try to) cancel pending DNS lookup / connection attempt + if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) { + // overwrite callback arguments for PHP7+ only, so they do not show + // up in the Exception trace and do not cause a possible cyclic reference. + $_ = $reject = null; + + $promise->cancel(); + $promise = null; + } + } + ); + } +} diff --git a/vendor/react/socket/src/FdServer.php b/vendor/react/socket/src/FdServer.php new file mode 100644 index 0000000..8e46719 --- /dev/null +++ b/vendor/react/socket/src/FdServer.php @@ -0,0 +1,222 @@ +on('connection', function (ConnectionInterface $connection) { + * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * @see ServerInterface + * @see ConnectionInterface + * @internal + */ +final class FdServer extends EventEmitter implements ServerInterface +{ + private $master; + private $loop; + private $unix = false; + private $listening = false; + + /** + * Creates a socket server and starts listening on the given file descriptor + * + * This starts accepting new incoming connections on the given file descriptor. + * See also the `connection event` documented in the `ServerInterface` + * for more details. + * + * ```php + * $socket = new React\Socket\FdServer(3); + * ``` + * + * If the given FD is invalid or out of range, it will throw an `InvalidArgumentException`: + * + * ```php + * // throws InvalidArgumentException + * $socket = new React\Socket\FdServer(-1); + * ``` + * + * If the given FD appears to be valid, but listening on it fails (such as + * if the FD does not exist or does not refer to a socket server), it will + * throw a `RuntimeException`: + * + * ```php + * // throws RuntimeException because FD does not reference a socket server + * $socket = new React\Socket\FdServer(0, $loop); + * ``` + * + * Note that these error conditions may vary depending on your system and/or + * configuration. + * See the exception message and code for more details about the actual error + * condition. + * + * @param int|string $fd FD number such as `3` or as URL in the form of `php://fd/3` + * @param ?LoopInterface $loop + * @throws \InvalidArgumentException if the listening address is invalid + * @throws \RuntimeException if listening on this address fails (already in use etc.) + */ + public function __construct($fd, $loop = null) + { + if (\preg_match('#^php://fd/(\d+)$#', $fd, $m)) { + $fd = (int) $m[1]; + } + if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) { + throw new \InvalidArgumentException( + 'Invalid FD number given (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + ); + } + + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); + } + + $this->loop = $loop ?: Loop::get(); + + $errno = 0; + $errstr = ''; + \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { + // Match errstr from PHP's warning message. + // fopen(php://fd/3): Failed to open stream: Error duping file descriptor 3; possibly it doesn't exist: [9]: Bad file descriptor + \preg_match('/\[(\d+)\]: (.*)/', $error, $m); + $errno = isset($m[1]) ? (int) $m[1] : 0; + $errstr = isset($m[2]) ? $m[2] : $error; + }); + + $this->master = \fopen('php://fd/' . $fd, 'r+'); + + \restore_error_handler(); + + if (false === $this->master) { + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno), + $errno + ); + } + + $meta = \stream_get_meta_data($this->master); + if (!isset($meta['stream_type']) || $meta['stream_type'] !== 'tcp_socket') { + \fclose($this->master); + + $errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88; + $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket'; + + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (ENOTSOCK)', + $errno + ); + } + + // Socket should not have a peer address if this is a listening socket. + // Looks like this work-around is the closest we can get because PHP doesn't expose SO_ACCEPTCONN even with ext-sockets. + if (\stream_socket_get_name($this->master, true) !== false) { + \fclose($this->master); + + $errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106; + $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected'; + + throw new \RuntimeException( + 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (EISCONN)', + $errno + ); + } + + // Assume this is a Unix domain socket (UDS) when its listening address doesn't parse as a valid URL with a port. + // Looks like this work-around is the closest we can get because PHP doesn't expose SO_DOMAIN even with ext-sockets. + $this->unix = \parse_url($this->getAddress(), \PHP_URL_PORT) === false; + + \stream_set_blocking($this->master, false); + + $this->resume(); + } + + public function getAddress() + { + if (!\is_resource($this->master)) { + return null; + } + + $address = \stream_socket_get_name($this->master, false); + + if ($this->unix === true) { + return 'unix://' . $address; + } + + // check if this is an IPv6 address which includes multiple colons but no square brackets + $pos = \strrpos($address, ':'); + if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') { + $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore + } + + return 'tcp://' . $address; + } + + public function pause() + { + if (!$this->listening) { + return; + } + + $this->loop->removeReadStream($this->master); + $this->listening = false; + } + + public function resume() + { + if ($this->listening || !\is_resource($this->master)) { + return; + } + + $that = $this; + $this->loop->addReadStream($this->master, function ($master) use ($that) { + try { + $newSocket = SocketServer::accept($master); + } catch (\RuntimeException $e) { + $that->emit('error', array($e)); + return; + } + $that->handleConnection($newSocket); + }); + $this->listening = true; + } + + public function close() + { + if (!\is_resource($this->master)) { + return; + } + + $this->pause(); + \fclose($this->master); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleConnection($socket) + { + $connection = new Connection($socket, $this->loop); + $connection->unix = $this->unix; + + $this->emit('connection', array($connection)); + } +} diff --git a/vendor/react/socket/src/FixedUriConnector.php b/vendor/react/socket/src/FixedUriConnector.php new file mode 100644 index 0000000..f83241d --- /dev/null +++ b/vendor/react/socket/src/FixedUriConnector.php @@ -0,0 +1,41 @@ +connect('localhost:80'); + * ``` + */ +class FixedUriConnector implements ConnectorInterface +{ + private $uri; + private $connector; + + /** + * @param string $uri + * @param ConnectorInterface $connector + */ + public function __construct($uri, ConnectorInterface $connector) + { + $this->uri = $uri; + $this->connector = $connector; + } + + public function connect($_) + { + return $this->connector->connect($this->uri); + } +} diff --git a/vendor/react/socket/src/HappyEyeBallsConnectionBuilder.php b/vendor/react/socket/src/HappyEyeBallsConnectionBuilder.php new file mode 100644 index 0000000..d4f05e8 --- /dev/null +++ b/vendor/react/socket/src/HappyEyeBallsConnectionBuilder.php @@ -0,0 +1,334 @@ + false, + Message::TYPE_AAAA => false, + ); + public $resolverPromises = array(); + public $connectionPromises = array(); + public $connectQueue = array(); + public $nextAttemptTimer; + public $parts; + public $ipsCount = 0; + public $failureCount = 0; + public $resolve; + public $reject; + + public $lastErrorFamily; + public $lastError6; + public $lastError4; + + public function __construct(LoopInterface $loop, ConnectorInterface $connector, ResolverInterface $resolver, $uri, $host, $parts) + { + $this->loop = $loop; + $this->connector = $connector; + $this->resolver = $resolver; + $this->uri = $uri; + $this->host = $host; + $this->parts = $parts; + } + + public function connect() + { + $that = $this; + return new Promise\Promise(function ($resolve, $reject) use ($that) { + $lookupResolve = function ($type) use ($that, $resolve, $reject) { + return function (array $ips) use ($that, $type, $resolve, $reject) { + unset($that->resolverPromises[$type]); + $that->resolved[$type] = true; + + $that->mixIpsIntoConnectQueue($ips); + + // start next connection attempt if not already awaiting next + if ($that->nextAttemptTimer === null && $that->connectQueue) { + $that->check($resolve, $reject); + } + }; + }; + + $that->resolverPromises[Message::TYPE_AAAA] = $that->resolve(Message::TYPE_AAAA, $reject)->then($lookupResolve(Message::TYPE_AAAA)); + $that->resolverPromises[Message::TYPE_A] = $that->resolve(Message::TYPE_A, $reject)->then(function (array $ips) use ($that) { + // happy path: IPv6 has resolved already (or could not resolve), continue with IPv4 addresses + if ($that->resolved[Message::TYPE_AAAA] === true || !$ips) { + return $ips; + } + + // Otherwise delay processing IPv4 lookup until short timer passes or IPv6 resolves in the meantime + $deferred = new Promise\Deferred(function () use (&$ips) { + // discard all IPv4 addresses if cancelled + $ips = array(); + }); + $timer = $that->loop->addTimer($that::RESOLUTION_DELAY, function () use ($deferred, $ips) { + $deferred->resolve($ips); + }); + + $that->resolverPromises[Message::TYPE_AAAA]->then(function () use ($that, $timer, $deferred, &$ips) { + $that->loop->cancelTimer($timer); + $deferred->resolve($ips); + }); + + return $deferred->promise(); + })->then($lookupResolve(Message::TYPE_A)); + }, function ($_, $reject) use ($that) { + $reject(new \RuntimeException( + 'Connection to ' . $that->uri . ' cancelled' . (!$that->connectionPromises ? ' during DNS lookup' : '') . ' (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); + $_ = $reject = null; + + $that->cleanUp(); + }); + } + + /** + * @internal + * @param int $type DNS query type + * @param callable $reject + * @return \React\Promise\PromiseInterface Returns a promise that + * always resolves with a list of IP addresses on success or an empty + * list on error. + */ + public function resolve($type, $reject) + { + $that = $this; + return $that->resolver->resolveAll($that->host, $type)->then(null, function (\Exception $e) use ($type, $reject, $that) { + unset($that->resolverPromises[$type]); + $that->resolved[$type] = true; + + if ($type === Message::TYPE_A) { + $that->lastError4 = $e->getMessage(); + $that->lastErrorFamily = 4; + } else { + $that->lastError6 = $e->getMessage(); + $that->lastErrorFamily = 6; + } + + // cancel next attempt timer when there are no more IPs to connect to anymore + if ($that->nextAttemptTimer !== null && !$that->connectQueue) { + $that->loop->cancelTimer($that->nextAttemptTimer); + $that->nextAttemptTimer = null; + } + + if ($that->hasBeenResolved() && $that->ipsCount === 0) { + $reject(new \RuntimeException( + $that->error(), + 0, + $e + )); + } + + // Exception already handled above, so don't throw an unhandled rejection here + return array(); + }); + } + + /** + * @internal + */ + public function check($resolve, $reject) + { + $ip = \array_shift($this->connectQueue); + + // start connection attempt and remember array position to later unset again + $this->connectionPromises[] = $this->attemptConnection($ip); + \end($this->connectionPromises); + $index = \key($this->connectionPromises); + + $that = $this; + $that->connectionPromises[$index]->then(function ($connection) use ($that, $index, $resolve) { + unset($that->connectionPromises[$index]); + + $that->cleanUp(); + + $resolve($connection); + }, function (\Exception $e) use ($that, $index, $ip, $resolve, $reject) { + unset($that->connectionPromises[$index]); + + $that->failureCount++; + + $message = \preg_replace('/^(Connection to [^ ]+)[&?]hostname=[^ &]+/', '$1', $e->getMessage()); + if (\strpos($ip, ':') === false) { + $that->lastError4 = $message; + $that->lastErrorFamily = 4; + } else { + $that->lastError6 = $message; + $that->lastErrorFamily = 6; + } + + // start next connection attempt immediately on error + if ($that->connectQueue) { + if ($that->nextAttemptTimer !== null) { + $that->loop->cancelTimer($that->nextAttemptTimer); + $that->nextAttemptTimer = null; + } + + $that->check($resolve, $reject); + } + + if ($that->hasBeenResolved() === false) { + return; + } + + if ($that->ipsCount === $that->failureCount) { + $that->cleanUp(); + + $reject(new \RuntimeException( + $that->error(), + $e->getCode(), + $e + )); + } + }); + + // Allow next connection attempt in 100ms: https://tools.ietf.org/html/rfc8305#section-5 + // Only start timer when more IPs are queued or when DNS query is still pending (might add more IPs) + if ($this->nextAttemptTimer === null && (\count($this->connectQueue) > 0 || $this->resolved[Message::TYPE_A] === false || $this->resolved[Message::TYPE_AAAA] === false)) { + $this->nextAttemptTimer = $this->loop->addTimer(self::CONNECTION_ATTEMPT_DELAY, function () use ($that, $resolve, $reject) { + $that->nextAttemptTimer = null; + + if ($that->connectQueue) { + $that->check($resolve, $reject); + } + }); + } + } + + /** + * @internal + */ + public function attemptConnection($ip) + { + $uri = Connector::uri($this->parts, $this->host, $ip); + + return $this->connector->connect($uri); + } + + /** + * @internal + */ + public function cleanUp() + { + // clear list of outstanding IPs to avoid creating new connections + $this->connectQueue = array(); + + // cancel pending connection attempts + foreach ($this->connectionPromises as $connectionPromise) { + if ($connectionPromise instanceof PromiseInterface && \method_exists($connectionPromise, 'cancel')) { + $connectionPromise->cancel(); + } + } + + // cancel pending DNS resolution (cancel IPv4 first in case it is awaiting IPv6 resolution delay) + foreach (\array_reverse($this->resolverPromises) as $resolverPromise) { + if ($resolverPromise instanceof PromiseInterface && \method_exists($resolverPromise, 'cancel')) { + $resolverPromise->cancel(); + } + } + + if ($this->nextAttemptTimer instanceof TimerInterface) { + $this->loop->cancelTimer($this->nextAttemptTimer); + $this->nextAttemptTimer = null; + } + } + + /** + * @internal + */ + public function hasBeenResolved() + { + foreach ($this->resolved as $typeHasBeenResolved) { + if ($typeHasBeenResolved === false) { + return false; + } + } + + return true; + } + + /** + * Mixes an array of IP addresses into the connect queue in such a way they alternate when attempting to connect. + * The goal behind it is first attempt to connect to IPv6, then to IPv4, then to IPv6 again until one of those + * attempts succeeds. + * + * @link https://tools.ietf.org/html/rfc8305#section-4 + * + * @internal + */ + public function mixIpsIntoConnectQueue(array $ips) + { + \shuffle($ips); + $this->ipsCount += \count($ips); + $connectQueueStash = $this->connectQueue; + $this->connectQueue = array(); + while (\count($connectQueueStash) > 0 || \count($ips) > 0) { + if (\count($ips) > 0) { + $this->connectQueue[] = \array_shift($ips); + } + if (\count($connectQueueStash) > 0) { + $this->connectQueue[] = \array_shift($connectQueueStash); + } + } + } + + /** + * @internal + * @return string + */ + public function error() + { + if ($this->lastError4 === $this->lastError6) { + $message = $this->lastError6; + } elseif ($this->lastErrorFamily === 6) { + $message = 'Last error for IPv6: ' . $this->lastError6 . '. Previous error for IPv4: ' . $this->lastError4; + } else { + $message = 'Last error for IPv4: ' . $this->lastError4 . '. Previous error for IPv6: ' . $this->lastError6; + } + + if ($this->hasBeenResolved() && $this->ipsCount === 0) { + if ($this->lastError6 === $this->lastError4) { + $message = ' during DNS lookup: ' . $this->lastError6; + } else { + $message = ' during DNS lookup. ' . $message; + } + } else { + $message = ': ' . $message; + } + + return 'Connection to ' . $this->uri . ' failed' . $message; + } +} diff --git a/vendor/react/socket/src/HappyEyeBallsConnector.php b/vendor/react/socket/src/HappyEyeBallsConnector.php new file mode 100644 index 0000000..a5511ac --- /dev/null +++ b/vendor/react/socket/src/HappyEyeBallsConnector.php @@ -0,0 +1,80 @@ +loop = $loop ?: Loop::get(); + $this->connector = $connector; + $this->resolver = $resolver; + } + + public function connect($uri) + { + $original = $uri; + if (\strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + $parts = \parse_url($uri); + if (isset($parts['scheme'])) { + unset($parts['scheme']); + } + } else { + $parts = \parse_url($uri); + } + + if (!$parts || !isset($parts['host'])) { + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $original . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + $host = \trim($parts['host'], '[]'); + + // skip DNS lookup / URI manipulation if this URI already contains an IP + if (@\inet_pton($host) !== false) { + return $this->connector->connect($original); + } + + $builder = new HappyEyeBallsConnectionBuilder( + $this->loop, + $this->connector, + $this->resolver, + $uri, + $host, + $parts + ); + return $builder->connect(); + } +} diff --git a/vendor/react/socket/src/LimitingServer.php b/vendor/react/socket/src/LimitingServer.php new file mode 100644 index 0000000..d19000b --- /dev/null +++ b/vendor/react/socket/src/LimitingServer.php @@ -0,0 +1,203 @@ +on('connection', function (React\Socket\ConnectionInterface $connection) { + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * @see ServerInterface + * @see ConnectionInterface + */ +class LimitingServer extends EventEmitter implements ServerInterface +{ + private $connections = array(); + private $server; + private $limit; + + private $pauseOnLimit = false; + private $autoPaused = false; + private $manuPaused = false; + + /** + * Instantiates a new LimitingServer. + * + * You have to pass a maximum number of open connections to ensure + * the server will automatically reject (close) connections once this limit + * is exceeded. In this case, it will emit an `error` event to inform about + * this and no `connection` event will be emitted. + * + * ```php + * $server = new React\Socket\LimitingServer($server, 100); + * $server->on('connection', function (React\Socket\ConnectionInterface $connection) { + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * You MAY pass a `null` limit in order to put no limit on the number of + * open connections and keep accepting new connection until you run out of + * operating system resources (such as open file handles). This may be + * useful if you do not want to take care of applying a limit but still want + * to use the `getConnections()` method. + * + * You can optionally configure the server to pause accepting new + * connections once the connection limit is reached. In this case, it will + * pause the underlying server and no longer process any new connections at + * all, thus also no longer closing any excessive connections. + * The underlying operating system is responsible for keeping a backlog of + * pending connections until its limit is reached, at which point it will + * start rejecting further connections. + * Once the server is below the connection limit, it will continue consuming + * connections from the backlog and will process any outstanding data on + * each connection. + * This mode may be useful for some protocols that are designed to wait for + * a response message (such as HTTP), but may be less useful for other + * protocols that demand immediate responses (such as a "welcome" message in + * an interactive chat). + * + * ```php + * $server = new React\Socket\LimitingServer($server, 100, true); + * $server->on('connection', function (React\Socket\ConnectionInterface $connection) { + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * @param ServerInterface $server + * @param int|null $connectionLimit + * @param bool $pauseOnLimit + */ + public function __construct(ServerInterface $server, $connectionLimit, $pauseOnLimit = false) + { + $this->server = $server; + $this->limit = $connectionLimit; + if ($connectionLimit !== null) { + $this->pauseOnLimit = $pauseOnLimit; + } + + $this->server->on('connection', array($this, 'handleConnection')); + $this->server->on('error', array($this, 'handleError')); + } + + /** + * Returns an array with all currently active connections + * + * ```php + * foreach ($server->getConnection() as $connection) { + * $connection->write('Hi!'); + * } + * ``` + * + * @return ConnectionInterface[] + */ + public function getConnections() + { + return $this->connections; + } + + public function getAddress() + { + return $this->server->getAddress(); + } + + public function pause() + { + if (!$this->manuPaused) { + $this->manuPaused = true; + + if (!$this->autoPaused) { + $this->server->pause(); + } + } + } + + public function resume() + { + if ($this->manuPaused) { + $this->manuPaused = false; + + if (!$this->autoPaused) { + $this->server->resume(); + } + } + } + + public function close() + { + $this->server->close(); + } + + /** @internal */ + public function handleConnection(ConnectionInterface $connection) + { + // close connection if limit exceeded + if ($this->limit !== null && \count($this->connections) >= $this->limit) { + $this->handleError(new \OverflowException('Connection closed because server reached connection limit')); + $connection->close(); + return; + } + + $this->connections[] = $connection; + $that = $this; + $connection->on('close', function () use ($that, $connection) { + $that->handleDisconnection($connection); + }); + + // pause accepting new connections if limit exceeded + if ($this->pauseOnLimit && !$this->autoPaused && \count($this->connections) >= $this->limit) { + $this->autoPaused = true; + + if (!$this->manuPaused) { + $this->server->pause(); + } + } + + $this->emit('connection', array($connection)); + } + + /** @internal */ + public function handleDisconnection(ConnectionInterface $connection) + { + unset($this->connections[\array_search($connection, $this->connections)]); + + // continue accepting new connection if below limit + if ($this->autoPaused && \count($this->connections) < $this->limit) { + $this->autoPaused = false; + + if (!$this->manuPaused) { + $this->server->resume(); + } + } + } + + /** @internal */ + public function handleError(\Exception $error) + { + $this->emit('error', array($error)); + } +} diff --git a/vendor/react/socket/src/SecureConnector.php b/vendor/react/socket/src/SecureConnector.php new file mode 100644 index 0000000..08255ac --- /dev/null +++ b/vendor/react/socket/src/SecureConnector.php @@ -0,0 +1,132 @@ +connector = $connector; + $this->streamEncryption = new StreamEncryption($loop ?: Loop::get(), false); + $this->context = $context; + } + + public function connect($uri) + { + if (!\function_exists('stream_socket_enable_crypto')) { + return Promise\reject(new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)')); // @codeCoverageIgnore + } + + if (\strpos($uri, '://') === false) { + $uri = 'tls://' . $uri; + } + + $parts = \parse_url($uri); + if (!$parts || !isset($parts['scheme']) || $parts['scheme'] !== 'tls') { + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + $context = $this->context; + $encryption = $this->streamEncryption; + $connected = false; + /** @var \React\Promise\PromiseInterface $promise */ + $promise = $this->connector->connect( + \str_replace('tls://', '', $uri) + )->then(function (ConnectionInterface $connection) use ($context, $encryption, $uri, &$promise, &$connected) { + // (unencrypted) TCP/IP connection succeeded + $connected = true; + + if (!$connection instanceof Connection) { + $connection->close(); + throw new \UnexpectedValueException('Base connector does not use internal Connection class exposing stream resource'); + } + + // set required SSL/TLS context options + foreach ($context as $name => $value) { + \stream_context_set_option($connection->stream, 'ssl', $name, $value); + } + + // try to enable encryption + return $promise = $encryption->enable($connection)->then(null, function ($error) use ($connection, $uri) { + // establishing encryption failed => close invalid connection and return error + $connection->close(); + + throw new \RuntimeException( + 'Connection to ' . $uri . ' failed during TLS handshake: ' . $error->getMessage(), + $error->getCode() + ); + }); + }, function (\Exception $e) use ($uri) { + if ($e instanceof \RuntimeException) { + $message = \preg_replace('/^Connection to [^ ]+/', '', $e->getMessage()); + $e = new \RuntimeException( + 'Connection to ' . $uri . $message, + $e->getCode(), + $e + ); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $r->setAccessible(true); + $trace = $r->getValue($e); + + // Exception trace arguments are not available on some PHP 7.4 installs + // @codeCoverageIgnoreStart + foreach ($trace as $ti => $one) { + if (isset($one['args'])) { + foreach ($one['args'] as $ai => $arg) { + if ($arg instanceof \Closure) { + $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($e, $trace); + } + + throw $e; + }); + + return new \React\Promise\Promise( + function ($resolve, $reject) use ($promise) { + $promise->then($resolve, $reject); + }, + function ($_, $reject) use (&$promise, $uri, &$connected) { + if ($connected) { + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during TLS handshake (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + )); + } + + $promise->cancel(); + $promise = null; + } + ); + } +} diff --git a/vendor/react/socket/src/SecureServer.php b/vendor/react/socket/src/SecureServer.php new file mode 100644 index 0000000..5a202d2 --- /dev/null +++ b/vendor/react/socket/src/SecureServer.php @@ -0,0 +1,210 @@ +on('connection', function (React\Socket\ConnectionInterface $connection) { + * echo 'Secure connection from' . $connection->getRemoteAddress() . PHP_EOL; + * + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * Whenever a client fails to perform a successful TLS handshake, it will emit an + * `error` event and then close the underlying TCP/IP connection: + * + * ```php + * $server->on('error', function (Exception $e) { + * echo 'Error' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * Note that the `SecureServer` class is a concrete implementation for TLS sockets. + * If you want to typehint in your higher-level protocol implementation, you SHOULD + * use the generic `ServerInterface` instead. + * + * @see ServerInterface + * @see ConnectionInterface + */ +final class SecureServer extends EventEmitter implements ServerInterface +{ + private $tcp; + private $encryption; + private $context; + + /** + * Creates a secure TLS server and starts waiting for incoming connections + * + * It does so by wrapping a `TcpServer` instance which waits for plaintext + * TCP/IP connections and then performs a TLS handshake for each connection. + * It thus requires valid [TLS context options], + * which in its most basic form may look something like this if you're using a + * PEM encoded certificate file: + * + * ```php + * $server = new React\Socket\TcpServer(8000); + * $server = new React\Socket\SecureServer($server, null, array( + * 'local_cert' => 'server.pem' + * )); + * ``` + * + * Note that the certificate file will not be loaded on instantiation but when an + * incoming connection initializes its TLS context. + * This implies that any invalid certificate file paths or contents will only cause + * an `error` event at a later time. + * + * If your private key is encrypted with a passphrase, you have to specify it + * like this: + * + * ```php + * $server = new React\Socket\TcpServer(8000); + * $server = new React\Socket\SecureServer($server, null, array( + * 'local_cert' => 'server.pem', + * 'passphrase' => 'secret' + * )); + * ``` + * + * Note that available [TLS context options], + * their defaults and effects of changing these may vary depending on your system + * and/or PHP version. + * Passing unknown context options has no effect. + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * Advanced usage: Despite allowing any `ServerInterface` as first parameter, + * you SHOULD pass a `TcpServer` instance as first parameter, unless you + * know what you're doing. + * Internally, the `SecureServer` has to set the required TLS context options on + * the underlying stream resources. + * These resources are not exposed through any of the interfaces defined in this + * package, but only through the internal `Connection` class. + * The `TcpServer` class is guaranteed to emit connections that implement + * the `ConnectionInterface` and uses the internal `Connection` class in order to + * expose these underlying resources. + * If you use a custom `ServerInterface` and its `connection` event does not + * meet this requirement, the `SecureServer` will emit an `error` event and + * then close the underlying connection. + * + * @param ServerInterface|TcpServer $tcp + * @param ?LoopInterface $loop + * @param array $context + * @throws BadMethodCallException for legacy HHVM < 3.8 due to lack of support + * @see TcpServer + * @link https://www.php.net/manual/en/context.ssl.php for TLS context options + */ + public function __construct(ServerInterface $tcp, $loop = null, array $context = array()) + { + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); + } + + if (!\function_exists('stream_socket_enable_crypto')) { + throw new \BadMethodCallException('Encryption not supported on your platform (HHVM < 3.8?)'); // @codeCoverageIgnore + } + + // default to empty passphrase to suppress blocking passphrase prompt + $context += array( + 'passphrase' => '' + ); + + $this->tcp = $tcp; + $this->encryption = new StreamEncryption($loop ?: Loop::get()); + $this->context = $context; + + $that = $this; + $this->tcp->on('connection', function ($connection) use ($that) { + $that->handleConnection($connection); + }); + $this->tcp->on('error', function ($error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function getAddress() + { + $address = $this->tcp->getAddress(); + if ($address === null) { + return null; + } + + return \str_replace('tcp://' , 'tls://', $address); + } + + public function pause() + { + $this->tcp->pause(); + } + + public function resume() + { + $this->tcp->resume(); + } + + public function close() + { + return $this->tcp->close(); + } + + /** @internal */ + public function handleConnection(ConnectionInterface $connection) + { + if (!$connection instanceof Connection) { + $this->emit('error', array(new \UnexpectedValueException('Base server does not use internal Connection class exposing stream resource'))); + $connection->close(); + return; + } + + foreach ($this->context as $name => $value) { + \stream_context_set_option($connection->stream, 'ssl', $name, $value); + } + + // get remote address before starting TLS handshake in case connection closes during handshake + $remote = $connection->getRemoteAddress(); + $that = $this; + + $this->encryption->enable($connection)->then( + function ($conn) use ($that) { + $that->emit('connection', array($conn)); + }, + function ($error) use ($that, $connection, $remote) { + $error = new \RuntimeException( + 'Connection from ' . $remote . ' failed during TLS handshake: ' . $error->getMessage(), + $error->getCode() + ); + + $that->emit('error', array($error)); + $connection->close(); + } + ); + } +} diff --git a/vendor/react/socket/src/Server.php b/vendor/react/socket/src/Server.php new file mode 100644 index 0000000..b24c556 --- /dev/null +++ b/vendor/react/socket/src/Server.php @@ -0,0 +1,118 @@ + $context); + } + + // apply default options if not explicitly given + $context += array( + 'tcp' => array(), + 'tls' => array(), + 'unix' => array() + ); + + $scheme = 'tcp'; + $pos = \strpos($uri, '://'); + if ($pos !== false) { + $scheme = \substr($uri, 0, $pos); + } + + if ($scheme === 'unix') { + $server = new UnixServer($uri, $loop, $context['unix']); + } else { + $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']); + + if ($scheme === 'tls') { + $server = new SecureServer($server, $loop, $context['tls']); + } + } + + $this->server = $server; + + $that = $this; + $server->on('connection', function (ConnectionInterface $conn) use ($that) { + $that->emit('connection', array($conn)); + }); + $server->on('error', function (Exception $error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function getAddress() + { + return $this->server->getAddress(); + } + + public function pause() + { + $this->server->pause(); + } + + public function resume() + { + $this->server->resume(); + } + + public function close() + { + $this->server->close(); + } +} diff --git a/vendor/react/socket/src/ServerInterface.php b/vendor/react/socket/src/ServerInterface.php new file mode 100644 index 0000000..aa79fa1 --- /dev/null +++ b/vendor/react/socket/src/ServerInterface.php @@ -0,0 +1,151 @@ +on('connection', function (React\Socket\ConnectionInterface $connection) { + * echo 'new connection' . PHP_EOL; + * }); + * ``` + * + * See also the `ConnectionInterface` for more details about handling the + * incoming connection. + * + * error event: + * The `error` event will be emitted whenever there's an error accepting a new + * connection from a client. + * + * ```php + * $socket->on('error', function (Exception $e) { + * echo 'error: ' . $e->getMessage() . PHP_EOL; + * }); + * ``` + * + * Note that this is not a fatal error event, i.e. the server keeps listening for + * new connections even after this event. + * + * @see ConnectionInterface + */ +interface ServerInterface extends EventEmitterInterface +{ + /** + * Returns the full address (URI) this server is currently listening on + * + * ```php + * $address = $socket->getAddress(); + * echo 'Server listening on ' . $address . PHP_EOL; + * ``` + * + * If the address can not be determined or is unknown at this time (such as + * after the socket has been closed), it MAY return a `NULL` value instead. + * + * Otherwise, it will return the full address (URI) as a string value, such + * as `tcp://127.0.0.1:8080`, `tcp://[::1]:80` or `tls://127.0.0.1:443`. + * Note that individual URI components are application specific and depend + * on the underlying transport protocol. + * + * If this is a TCP/IP based server and you only want the local port, you may + * use something like this: + * + * ```php + * $address = $socket->getAddress(); + * $port = parse_url($address, PHP_URL_PORT); + * echo 'Server listening on port ' . $port . PHP_EOL; + * ``` + * + * @return ?string the full listening address (URI) or NULL if it is unknown (not applicable to this server socket or already closed) + */ + public function getAddress(); + + /** + * Pauses accepting new incoming connections. + * + * Removes the socket resource from the EventLoop and thus stop accepting + * new connections. Note that the listening socket stays active and is not + * closed. + * + * This means that new incoming connections will stay pending in the + * operating system backlog until its configurable backlog is filled. + * Once the backlog is filled, the operating system may reject further + * incoming connections until the backlog is drained again by resuming + * to accept new connections. + * + * Once the server is paused, no futher `connection` events SHOULD + * be emitted. + * + * ```php + * $socket->pause(); + * + * $socket->on('connection', assertShouldNeverCalled()); + * ``` + * + * This method is advisory-only, though generally not recommended, the + * server MAY continue emitting `connection` events. + * + * Unless otherwise noted, a successfully opened server SHOULD NOT start + * in paused state. + * + * You can continue processing events by calling `resume()` again. + * + * Note that both methods can be called any number of times, in particular + * calling `pause()` more than once SHOULD NOT have any effect. + * Similarly, calling this after `close()` is a NO-OP. + * + * @see self::resume() + * @return void + */ + public function pause(); + + /** + * Resumes accepting new incoming connections. + * + * Re-attach the socket resource to the EventLoop after a previous `pause()`. + * + * ```php + * $socket->pause(); + * + * Loop::addTimer(1.0, function () use ($socket) { + * $socket->resume(); + * }); + * ``` + * + * Note that both methods can be called any number of times, in particular + * calling `resume()` without a prior `pause()` SHOULD NOT have any effect. + * Similarly, calling this after `close()` is a NO-OP. + * + * @see self::pause() + * @return void + */ + public function resume(); + + /** + * Shuts down this listening socket + * + * This will stop listening for new incoming connections on this socket. + * + * Calling this method more than once on the same instance is a NO-OP. + * + * @return void + */ + public function close(); +} diff --git a/vendor/react/socket/src/SocketServer.php b/vendor/react/socket/src/SocketServer.php new file mode 100644 index 0000000..e987f5f --- /dev/null +++ b/vendor/react/socket/src/SocketServer.php @@ -0,0 +1,215 @@ + array(), + 'tls' => array(), + 'unix' => array() + ); + + $scheme = 'tcp'; + $pos = \strpos($uri, '://'); + if ($pos !== false) { + $scheme = \substr($uri, 0, $pos); + } + + if ($scheme === 'unix') { + $server = new UnixServer($uri, $loop, $context['unix']); + } elseif ($scheme === 'php') { + $server = new FdServer($uri, $loop); + } else { + if (preg_match('#^(?:\w+://)?\d+$#', $uri)) { + throw new \InvalidArgumentException( + 'Invalid URI given (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + ); + } + + $server = new TcpServer(str_replace('tls://', '', $uri), $loop, $context['tcp']); + + if ($scheme === 'tls') { + $server = new SecureServer($server, $loop, $context['tls']); + } + } + + $this->server = $server; + + $that = $this; + $server->on('connection', function (ConnectionInterface $conn) use ($that) { + $that->emit('connection', array($conn)); + }); + $server->on('error', function (\Exception $error) use ($that) { + $that->emit('error', array($error)); + }); + } + + public function getAddress() + { + return $this->server->getAddress(); + } + + public function pause() + { + $this->server->pause(); + } + + public function resume() + { + $this->server->resume(); + } + + public function close() + { + $this->server->close(); + } + + /** + * [internal] Internal helper method to accept new connection from given server socket + * + * @param resource $socket server socket to accept connection from + * @return resource new client socket if any + * @throws \RuntimeException if accepting fails + * @internal + */ + public static function accept($socket) + { + $errno = 0; + $errstr = ''; + \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { + // Match errstr from PHP's warning message. + // stream_socket_accept(): accept failed: Connection timed out + $errstr = \preg_replace('#.*: #', '', $error); + $errno = SocketServer::errno($errstr); + }); + + $newSocket = \stream_socket_accept($socket, 0); + + \restore_error_handler(); + + if (false === $newSocket) { + throw new \RuntimeException( + 'Unable to accept new connection: ' . $errstr . self::errconst($errno), + $errno + ); + } + + return $newSocket; + } + + /** + * [Internal] Returns errno value for given errstr + * + * The errno and errstr values describes the type of error that has been + * encountered. This method tries to look up the given errstr and find a + * matching errno value which can be useful to provide more context to error + * messages. It goes through the list of known errno constants when either + * `ext-sockets`, `ext-posix` or `ext-pcntl` is available to find an errno + * matching the given errstr. + * + * @param string $errstr + * @return int errno value (e.g. value of `SOCKET_ECONNREFUSED`) or 0 if not found + * @internal + * @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/errno with permission + * @codeCoverageIgnore + */ + public static function errno($errstr) + { + // PHP defines the required `strerror()` function through either `ext-sockets`, `ext-posix` or `ext-pcntl` + $strerror = \function_exists('socket_strerror') ? 'socket_strerror' : (\function_exists('posix_strerror') ? 'posix_strerror' : (\function_exists('pcntl_strerror') ? 'pcntl_strerror' : null)); + if ($strerror !== null) { + assert(\is_string($strerror) && \is_callable($strerror)); + + // PHP defines most useful errno constants like `ECONNREFUSED` through constants in `ext-sockets` like `SOCKET_ECONNREFUSED` + // PHP also defines a hand full of errno constants like `EMFILE` through constants in `ext-pcntl` like `PCNTL_EMFILE` + // go through list of all defined constants like `SOCKET_E*` and `PCNTL_E*` and see if they match the given `$errstr` + foreach (\get_defined_constants(false) as $name => $value) { + if (\is_int($value) && (\strpos($name, 'SOCKET_E') === 0 || \strpos($name, 'PCNTL_E') === 0) && $strerror($value) === $errstr) { + return $value; + } + } + + // if we reach this, no matching errno constant could be found (unlikely when `ext-sockets` is available) + // go through list of all possible errno values from 1 to `MAX_ERRNO` and see if they match the given `$errstr` + for ($errno = 1, $max = \defined('MAX_ERRNO') ? \MAX_ERRNO : 4095; $errno <= $max; ++$errno) { + if ($strerror($errno) === $errstr) { + return $errno; + } + } + } + + // if we reach this, no matching errno value could be found (unlikely when either `ext-sockets`, `ext-posix` or `ext-pcntl` is available) + return 0; + } + + /** + * [Internal] Returns errno constant name for given errno value + * + * The errno value describes the type of error that has been encountered. + * This method tries to look up the given errno value and find a matching + * errno constant name which can be useful to provide more context and more + * descriptive error messages. It goes through the list of known errno + * constants when either `ext-sockets` or `ext-pcntl` is available to find + * the matching errno constant name. + * + * Because this method is used to append more context to error messages, the + * constant name will be prefixed with a space and put between parenthesis + * when found. + * + * @param int $errno + * @return string e.g. ` (ECONNREFUSED)` or empty string if no matching const for the given errno could be found + * @internal + * @copyright Copyright (c) 2023 Christian Lück, taken from https://github.com/clue/errno with permission + * @codeCoverageIgnore + */ + public static function errconst($errno) + { + // PHP defines most useful errno constants like `ECONNREFUSED` through constants in `ext-sockets` like `SOCKET_ECONNREFUSED` + // PHP also defines a hand full of errno constants like `EMFILE` through constants in `ext-pcntl` like `PCNTL_EMFILE` + // go through list of all defined constants like `SOCKET_E*` and `PCNTL_E*` and see if they match the given `$errno` + foreach (\get_defined_constants(false) as $name => $value) { + if ($value === $errno && (\strpos($name, 'SOCKET_E') === 0 || \strpos($name, 'PCNTL_E') === 0)) { + return ' (' . \substr($name, \strpos($name, '_') + 1) . ')'; + } + } + + // if we reach this, no matching errno constant could be found (unlikely when `ext-sockets` is available) + return ''; + } +} diff --git a/vendor/react/socket/src/StreamEncryption.php b/vendor/react/socket/src/StreamEncryption.php new file mode 100644 index 0000000..f91a359 --- /dev/null +++ b/vendor/react/socket/src/StreamEncryption.php @@ -0,0 +1,158 @@ +loop = $loop; + $this->server = $server; + + // support TLSv1.0+ by default and exclude legacy SSLv2/SSLv3. + // As of PHP 7.2+ the main crypto method constant includes all TLS versions. + // As of PHP 5.6+ the crypto method is a bitmask, so we explicitly include all TLS versions. + // For legacy PHP < 5.6 the crypto method is a single value only and this constant includes all TLS versions. + // @link https://3v4l.org/9PSST + if ($server) { + $this->method = \STREAM_CRYPTO_METHOD_TLS_SERVER; + + if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) { + $this->method |= \STREAM_CRYPTO_METHOD_TLSv1_0_SERVER | \STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | \STREAM_CRYPTO_METHOD_TLSv1_2_SERVER; // @codeCoverageIgnore + } + } else { + $this->method = \STREAM_CRYPTO_METHOD_TLS_CLIENT; + + if (\PHP_VERSION_ID < 70200 && \PHP_VERSION_ID >= 50600) { + $this->method |= \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; // @codeCoverageIgnore + } + } + } + + /** + * @param Connection $stream + * @return \React\Promise\PromiseInterface + */ + public function enable(Connection $stream) + { + return $this->toggle($stream, true); + } + + /** + * @param Connection $stream + * @param bool $toggle + * @return \React\Promise\PromiseInterface + */ + public function toggle(Connection $stream, $toggle) + { + // pause actual stream instance to continue operation on raw stream socket + $stream->pause(); + + // TODO: add write() event to make sure we're not sending any excessive data + + // cancelling this leaves this stream in an inconsistent state… + $deferred = new Deferred(function () { + throw new \RuntimeException(); + }); + + // get actual stream socket from stream instance + $socket = $stream->stream; + + // get crypto method from context options or use global setting from constructor + $method = $this->method; + $context = \stream_context_get_options($socket); + if (isset($context['ssl']['crypto_method'])) { + $method = $context['ssl']['crypto_method']; + } + + $that = $this; + $toggleCrypto = function () use ($socket, $deferred, $toggle, $method, $that) { + $that->toggleCrypto($socket, $deferred, $toggle, $method); + }; + + $this->loop->addReadStream($socket, $toggleCrypto); + + if (!$this->server) { + $toggleCrypto(); + } + + $loop = $this->loop; + + return $deferred->promise()->then(function () use ($stream, $socket, $loop, $toggle) { + $loop->removeReadStream($socket); + + $stream->encryptionEnabled = $toggle; + $stream->resume(); + + return $stream; + }, function($error) use ($stream, $socket, $loop) { + $loop->removeReadStream($socket); + $stream->resume(); + throw $error; + }); + } + + /** + * @internal + * @param resource $socket + * @param Deferred $deferred + * @param bool $toggle + * @param int $method + * @return void + */ + public function toggleCrypto($socket, Deferred $deferred, $toggle, $method) + { + $error = null; + \set_error_handler(function ($_, $errstr) use (&$error) { + $error = \str_replace(array("\r", "\n"), ' ', $errstr); + + // remove useless function name from error message + if (($pos = \strpos($error, "): ")) !== false) { + $error = \substr($error, $pos + 3); + } + }); + + $result = \stream_socket_enable_crypto($socket, $toggle, $method); + + \restore_error_handler(); + + if (true === $result) { + $deferred->resolve(null); + } else if (false === $result) { + // overwrite callback arguments for PHP7+ only, so they do not show + // up in the Exception trace and do not cause a possible cyclic reference. + $d = $deferred; + $deferred = null; + + if (\feof($socket) || $error === null) { + // EOF or failed without error => connection closed during handshake + $d->reject(new \UnexpectedValueException( + 'Connection lost during TLS handshake (ECONNRESET)', + \defined('SOCKET_ECONNRESET') ? \SOCKET_ECONNRESET : 104 + )); + } else { + // handshake failed with error message + $d->reject(new \UnexpectedValueException( + $error + )); + } + } else { + // need more data, will retry + } + } +} diff --git a/vendor/react/socket/src/TcpConnector.php b/vendor/react/socket/src/TcpConnector.php new file mode 100644 index 0000000..9d2599e --- /dev/null +++ b/vendor/react/socket/src/TcpConnector.php @@ -0,0 +1,173 @@ +loop = $loop ?: Loop::get(); + $this->context = $context; + } + + 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( + 'Given URI "' . $uri . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + $ip = \trim($parts['host'], '[]'); + if (@\inet_pton($ip) === false) { + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + // use context given in constructor + $context = array( + 'socket' => $this->context + ); + + // parse arguments from query component of URI + $args = array(); + if (isset($parts['query'])) { + \parse_str($parts['query'], $args); + } + + // If an original hostname has been given, use this for TLS setup. + // This can happen due to layers of nested connectors, such as a + // DnsConnector reporting its original hostname. + // These context options are here in case TLS is enabled later on this stream. + // If TLS is not enabled later, this doesn't hurt either. + if (isset($args['hostname'])) { + $context['ssl'] = array( + 'SNI_enabled' => true, + 'peer_name' => $args['hostname'] + ); + + // Legacy PHP < 5.6 ignores peer_name and requires legacy context options instead. + // The SNI_server_name context option has to be set here during construction, + // as legacy PHP ignores any values set later. + // @codeCoverageIgnoreStart + if (\PHP_VERSION_ID < 50600) { + $context['ssl'] += array( + 'SNI_server_name' => $args['hostname'], + 'CN_match' => $args['hostname'] + ); + } + // @codeCoverageIgnoreEnd + } + + // latest versions of PHP no longer accept any other URI components and + // HHVM fails to parse URIs with a query but no path, so let's simplify our URI here + $remote = 'tcp://' . $parts['host'] . ':' . $parts['port']; + + $stream = @\stream_socket_client( + $remote, + $errno, + $errstr, + 0, + \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT, + \stream_context_create($context) + ); + + if (false === $stream) { + return Promise\reject(new \RuntimeException( + 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno), + $errno + )); + } + + // wait for connection + $loop = $this->loop; + return new Promise\Promise(function ($resolve, $reject) use ($loop, $stream, $uri) { + $loop->addWriteStream($stream, function ($stream) use ($loop, $resolve, $reject, $uri) { + $loop->removeWriteStream($stream); + + // The following hack looks like the only way to + // detect connection refused errors with PHP's stream sockets. + if (false === \stream_socket_get_name($stream, true)) { + // If we reach this point, we know the connection is dead, but we don't know the underlying error condition. + // @codeCoverageIgnoreStart + if (\function_exists('socket_import_stream')) { + // actual socket errno and errstr can be retrieved with ext-sockets on PHP 5.4+ + $socket = \socket_import_stream($stream); + $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR); + $errstr = \socket_strerror($errno); + } elseif (\PHP_OS === 'Linux') { + // Linux reports socket errno and errstr again when trying to write to the dead socket. + // Suppress error reporting to get error message below and close dead socket before rejecting. + // This is only known to work on Linux, Mac and Windows are known to not support this. + $errno = 0; + $errstr = ''; + \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { + // Match errstr from PHP's warning message. + // fwrite(): send of 1 bytes failed with errno=111 Connection refused + \preg_match('/errno=(\d+) (.+)/', $error, $m); + $errno = isset($m[1]) ? (int) $m[1] : 0; + $errstr = isset($m[2]) ? $m[2] : $error; + }); + + \fwrite($stream, \PHP_EOL); + + \restore_error_handler(); + } else { + // Not on Linux and ext-sockets not available? Too bad. + $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111; + $errstr = 'Connection refused?'; + } + // @codeCoverageIgnoreEnd + + \fclose($stream); + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno), + $errno + )); + } else { + $resolve(new Connection($stream, $loop)); + } + }); + }, function () use ($loop, $stream, $uri) { + $loop->removeWriteStream($stream); + \fclose($stream); + + // @codeCoverageIgnoreStart + // legacy PHP 5.3 sometimes requires a second close call (see tests) + if (\PHP_VERSION_ID < 50400 && \is_resource($stream)) { + \fclose($stream); + } + // @codeCoverageIgnoreEnd + + throw new \RuntimeException( + 'Connection to ' . $uri . ' cancelled during TCP/IP handshake (ECONNABORTED)', + \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103 + ); + }); + } +} diff --git a/vendor/react/socket/src/TcpServer.php b/vendor/react/socket/src/TcpServer.php new file mode 100644 index 0000000..01b2b46 --- /dev/null +++ b/vendor/react/socket/src/TcpServer.php @@ -0,0 +1,262 @@ +on('connection', function (React\Socket\ConnectionInterface $connection) { + * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; + * $connection->write('hello there!' . PHP_EOL); + * … + * }); + * ``` + * + * See also the `ServerInterface` for more details. + * + * @see ServerInterface + * @see ConnectionInterface + */ +final class TcpServer extends EventEmitter implements ServerInterface +{ + private $master; + private $loop; + private $listening = false; + + /** + * Creates a plaintext TCP/IP socket server and starts listening on the given address + * + * This starts accepting new incoming connections on the given address. + * See also the `connection event` documented in the `ServerInterface` + * for more details. + * + * ```php + * $server = new React\Socket\TcpServer(8080); + * ``` + * + * As above, the `$uri` parameter can consist of only a port, in which case the + * server will default to listening on the localhost address `127.0.0.1`, + * which means it will not be reachable from outside of this system. + * + * In order to use a random port assignment, you can use the port `0`: + * + * ```php + * $server = new React\Socket\TcpServer(0); + * $address = $server->getAddress(); + * ``` + * + * In order to change the host the socket is listening on, you can provide an IP + * address through the first parameter provided to the constructor, optionally + * preceded by the `tcp://` scheme: + * + * ```php + * $server = new React\Socket\TcpServer('192.168.0.1:8080'); + * ``` + * + * If you want to listen on an IPv6 address, you MUST enclose the host in square + * brackets: + * + * ```php + * $server = new React\Socket\TcpServer('[::1]:8080'); + * ``` + * + * If the given URI is invalid, does not contain a port, any other scheme or if it + * contains a hostname, it will throw an `InvalidArgumentException`: + * + * ```php + * // throws InvalidArgumentException due to missing port + * $server = new React\Socket\TcpServer('127.0.0.1'); + * ``` + * + * If the given URI appears to be valid, but listening on it fails (such as if port + * is already in use or port below 1024 may require root access etc.), it will + * throw a `RuntimeException`: + * + * ```php + * $first = new React\Socket\TcpServer(8080); + * + * // throws RuntimeException because port is already in use + * $second = new React\Socket\TcpServer(8080); + * ``` + * + * Note that these error conditions may vary depending on your system and/or + * configuration. + * See the exception message and code for more details about the actual error + * condition. + * + * 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. + * + * Optionally, you can specify [socket context options](https://www.php.net/manual/en/context.socket.php) + * for the underlying stream socket resource like this: + * + * ```php + * $server = new React\Socket\TcpServer('[::1]:8080', null, array( + * 'backlog' => 200, + * 'so_reuseport' => true, + * 'ipv6_v6only' => true + * )); + * ``` + * + * Note that available [socket context options](https://www.php.net/manual/en/context.socket.php), + * their defaults and effects of changing these may vary depending on your system + * and/or PHP version. + * Passing unknown context options has no effect. + * The `backlog` context option defaults to `511` unless given explicitly. + * + * @param string|int $uri + * @param ?LoopInterface $loop + * @param array $context + * @throws InvalidArgumentException if the listening address is invalid + * @throws RuntimeException if listening on this address fails (already in use etc.) + */ + public function __construct($uri, $loop = null, array $context = array()) + { + if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 + throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); + } + + $this->loop = $loop ?: Loop::get(); + + // a single port has been given => assume localhost + if ((string)(int)$uri === (string)$uri) { + $uri = '127.0.0.1:' . $uri; + } + + // assume default scheme if none has been given + if (\strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + // parse_url() does not accept null ports (random port assignment) => manually remove + if (\substr($uri, -2) === ':0') { + $parts = \parse_url(\substr($uri, 0, -2)); + if ($parts) { + $parts['port'] = 0; + } + } else { + $parts = \parse_url($uri); + } + + // ensure URI contains TCP scheme, host and port + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { + throw new \InvalidArgumentException( + 'Invalid URI "' . $uri . '" given (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + ); + } + + if (@\inet_pton(\trim($parts['host'], '[]')) === false) { + throw new \InvalidArgumentException( + 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + ); + } + + $this->master = @\stream_socket_server( + $uri, + $errno, + $errstr, + \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN, + \stream_context_create(array('socket' => $context + array('backlog' => 511))) + ); + if (false === $this->master) { + if ($errno === 0) { + // PHP does not seem to report errno, so match errno from errstr + // @link https://3v4l.org/3qOBl + $errno = SocketServer::errno($errstr); + } + + throw new \RuntimeException( + 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno), + $errno + ); + } + \stream_set_blocking($this->master, false); + + $this->resume(); + } + + public function getAddress() + { + if (!\is_resource($this->master)) { + return null; + } + + $address = \stream_socket_get_name($this->master, false); + + // check if this is an IPv6 address which includes multiple colons but no square brackets + $pos = \strrpos($address, ':'); + if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') { + $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore + } + + return 'tcp://' . $address; + } + + public function pause() + { + if (!$this->listening) { + return; + } + + $this->loop->removeReadStream($this->master); + $this->listening = false; + } + + public function resume() + { + if ($this->listening || !\is_resource($this->master)) { + return; + } + + $that = $this; + $this->loop->addReadStream($this->master, function ($master) use ($that) { + try { + $newSocket = SocketServer::accept($master); + } catch (\RuntimeException $e) { + $that->emit('error', array($e)); + return; + } + $that->handleConnection($newSocket); + }); + $this->listening = true; + } + + public function close() + { + if (!\is_resource($this->master)) { + return; + } + + $this->pause(); + \fclose($this->master); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleConnection($socket) + { + $this->emit('connection', array( + new Connection($socket, $this->loop) + )); + } +} diff --git a/vendor/react/socket/src/TimeoutConnector.php b/vendor/react/socket/src/TimeoutConnector.php new file mode 100644 index 0000000..9ef252f --- /dev/null +++ b/vendor/react/socket/src/TimeoutConnector.php @@ -0,0 +1,79 @@ +connector = $connector; + $this->timeout = $timeout; + $this->loop = $loop ?: Loop::get(); + } + + public function connect($uri) + { + $promise = $this->connector->connect($uri); + + $loop = $this->loop; + $time = $this->timeout; + return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $uri) { + $timer = null; + $promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) { + if ($timer) { + $loop->cancelTimer($timer); + } + $timer = false; + $resolve($v); + }, function ($v) use (&$timer, $loop, $reject) { + if ($timer) { + $loop->cancelTimer($timer); + } + $timer = false; + $reject($v); + }); + + // promise already resolved => no need to start timer + if ($timer === false) { + return; + } + + // start timeout timer which will cancel the pending promise + $timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $uri) { + $reject(new \RuntimeException( + 'Connection to ' . $uri . ' timed out after ' . $time . ' seconds (ETIMEDOUT)', + \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110 + )); + + // Cancel pending connection to clean up any underlying resources and references. + // Avoid garbage references in call stack by passing pending promise by reference. + assert(\method_exists($promise, 'cancel')); + $promise->cancel(); + $promise = null; + }); + }, function () use (&$promise) { + // Cancelling this promise will cancel the pending connection, thus triggering the rejection logic above. + // Avoid garbage references in call stack by passing pending promise by reference. + assert(\method_exists($promise, 'cancel')); + $promise->cancel(); + $promise = null; + }); + } +} diff --git a/vendor/react/socket/src/UnixConnector.php b/vendor/react/socket/src/UnixConnector.php new file mode 100644 index 0000000..95f932c --- /dev/null +++ b/vendor/react/socket/src/UnixConnector.php @@ -0,0 +1,58 @@ +loop = $loop ?: Loop::get(); + } + + public function connect($path) + { + if (\strpos($path, '://') === false) { + $path = 'unix://' . $path; + } elseif (\substr($path, 0, 7) !== 'unix://') { + return Promise\reject(new \InvalidArgumentException( + 'Given URI "' . $path . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + )); + } + + $resource = @\stream_socket_client($path, $errno, $errstr, 1.0); + + if (!$resource) { + return Promise\reject(new \RuntimeException( + 'Unable to connect to unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno), + $errno + )); + } + + $connection = new Connection($resource, $this->loop); + $connection->unix = true; + + return Promise\resolve($connection); + } +} diff --git a/vendor/react/socket/src/UnixServer.php b/vendor/react/socket/src/UnixServer.php new file mode 100644 index 0000000..27b014d --- /dev/null +++ b/vendor/react/socket/src/UnixServer.php @@ -0,0 +1,162 @@ +loop = $loop ?: Loop::get(); + + if (\strpos($path, '://') === false) { + $path = 'unix://' . $path; + } elseif (\substr($path, 0, 7) !== 'unix://') { + throw new \InvalidArgumentException( + 'Given URI "' . $path . '" is invalid (EINVAL)', + \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) + ); + } + + $errno = 0; + $errstr = ''; + \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { + // PHP does not seem to report errno/errstr for Unix domain sockets (UDS) right now. + // This only applies to UDS server sockets, see also https://3v4l.org/NAhpr. + // Parse PHP warning message containing unknown error, HHVM reports proper info at least. + if (\preg_match('/\(([^\)]+)\)|\[(\d+)\]: (.*)/', $error, $match)) { + $errstr = isset($match[3]) ? $match['3'] : $match[1]; + $errno = isset($match[2]) ? (int)$match[2] : 0; + } + }); + + $this->master = \stream_socket_server( + $path, + $errno, + $errstr, + \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN, + \stream_context_create(array('socket' => $context)) + ); + + \restore_error_handler(); + + if (false === $this->master) { + throw new \RuntimeException( + 'Failed to listen on Unix domain socket "' . $path . '": ' . $errstr . SocketServer::errconst($errno), + $errno + ); + } + \stream_set_blocking($this->master, 0); + + $this->resume(); + } + + public function getAddress() + { + if (!\is_resource($this->master)) { + return null; + } + + return 'unix://' . \stream_socket_get_name($this->master, false); + } + + public function pause() + { + if (!$this->listening) { + return; + } + + $this->loop->removeReadStream($this->master); + $this->listening = false; + } + + public function resume() + { + if ($this->listening || !is_resource($this->master)) { + return; + } + + $that = $this; + $this->loop->addReadStream($this->master, function ($master) use ($that) { + try { + $newSocket = SocketServer::accept($master); + } catch (\RuntimeException $e) { + $that->emit('error', array($e)); + return; + } + $that->handleConnection($newSocket); + }); + $this->listening = true; + } + + public function close() + { + if (!\is_resource($this->master)) { + return; + } + + $this->pause(); + \fclose($this->master); + $this->removeAllListeners(); + } + + /** @internal */ + public function handleConnection($socket) + { + $connection = new Connection($socket, $this->loop); + $connection->unix = true; + + $this->emit('connection', array( + $connection + )); + } +}