diff options
Diffstat (limited to 'vendor/clue/socks-react')
-rw-r--r-- | vendor/clue/socks-react/CHANGELOG.md | 481 | ||||
-rw-r--r-- | vendor/clue/socks-react/LICENSE | 21 | ||||
-rw-r--r-- | vendor/clue/socks-react/README.md | 1116 | ||||
-rw-r--r-- | vendor/clue/socks-react/composer.json | 31 | ||||
-rw-r--r-- | vendor/clue/socks-react/src/Client.php | 458 | ||||
-rw-r--r-- | vendor/clue/socks-react/src/Server.php | 416 | ||||
-rw-r--r-- | vendor/clue/socks-react/src/StreamReader.php | 149 |
7 files changed, 2672 insertions, 0 deletions
diff --git a/vendor/clue/socks-react/CHANGELOG.md b/vendor/clue/socks-react/CHANGELOG.md new file mode 100644 index 0000000..cd72e1d --- /dev/null +++ b/vendor/clue/socks-react/CHANGELOG.md @@ -0,0 +1,481 @@ +# Changelog + +## 1.4.0 (2022-08-31) + +* Feature: Full support for PHP 8.1 and PHP 8.2. + (#105 by @clue and #108 by @SimonFrings) + +* Feature: Mark passwords and URIs as `#[\SensitiveParameter]` (PHP 8.2+). + (#109 by @SimonFrings) + +* Feature: Forward compatibility with upcoming Promise v3. + (#106 by @clue) + +* Bug: Fix invalid references in exception stack trace. + (#104 by @clue) + +* Improve test suite and fix legacy HHVM build. + (#107 by @SimonFrings) + +## 1.3.0 (2021-08-06) + +* Feature: Simplify usage by supporting new default loop and making `Connector` optional. + (#100 and #101 by @clue) + + ```php + // old (still supported) + $proxy = new Clue\React\Socks\Client( + $url, + new React\Socket\Connector($loop) + ); + $server = new Clue\React\Socks\Server($loop); + $server->listen(new React\Socket\Server('127.0.0.1:1080', $loop)); + + // new (using default loop) + $proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + $socks = new Clue\React\Socks\Server(); + $socks->listen(new React\Socket\SocketServer('127.0.0.1:1080')); + ``` + +* Documentation improvements and updated examples. + (#98 and #102 by @clue and #99 by @PaulRotmann) + +* Improve test suite and use GitHub actions for continuous integration (CI). + (#97 by @SimonFrings) + +## 1.2.0 (2020-10-23) + +* Enhanced documentation for ReactPHP's new HTTP client. + (#95 by @SimonFrings) + +* Improve test suite, prepare PHP 8 support and support PHPUnit 9.3. + (#96 by @SimonFrings) + +## 1.1.0 (2020-06-19) + +* Feature / Fix: Support PHP 7.4 by skipping unneeded cleanup of exception trace args. + (#92 by @clue) + +* Clean up test suite and add `.gitattributes` to exclude dev files from exports. + Run tests on PHP 7.4, PHPUnit 9 and simplify test matrix. + Link to using SSH proxy (SSH tunnel) as an alternative. + (#88 by @clue and #91 and #93 by @SimonFrings) + +## 1.0.0 (2018-11-20) + +* First stable release, now following SemVer! + +* Feature / BC break: Unify SOCKS5 and SOCKS4(a) protocol version handling, + the `Client` now defaults to SOCKS5 instead of SOCKS4a, + remove explicit SOCKS4a handling and merge into SOCKS4 protocol handling and + URI scheme `socks5://` now only acts as an alias for default `socks://` scheme. + (#74, #81 and #87 by @clue) + + ```php + // old: defaults to SOCKS4a + $client = new Client('127.0.0.1:1080', $connector); + $client = new Client('socks://127.0.0.1:1080', $connector); + + // new: defaults to SOCKS5 + $client = new Client('127.0.0.1:1080', $connector); + $client = new Client('socks://127.0.0.1:1080', $connector); + + // new: explicitly use legacy SOCKS4(a) + $client = new Client('socks4://127.0.0.1:1080', $connector); + + // unchanged: explicitly use SOCKS5 + $client = new Client('socks5://127.0.0.1:1080', $connector); + ``` + +* Feature / BC break: Clean up `Server` interface, + add `Server::listen()` method instead of accepting socket in constructor, + replace `Server::setAuth()` with optional constructor parameter, + remove undocumented "connection" event from Server and drop explicit Evenement dependency and + mark all classes as `final` and all internal APIs as `@internal` + (#78, #79, #80 and #84 by @clue) + + ```php + // old: socket passed to server constructor + $socket = new React\Socket\Server(1080, $loop); + $server = new Clue\React\Socks\Server($loop, $socket); + + // old: authentication via setAuthArray()/setAuth() methods + $server = new Clue\React\Socks\Server($loop, $socket); + $server->setAuthArray(array( + 'tom' => 'password', + 'admin' => 'root' + )); + + // new: socket passed to listen() method + $server = new Clue\React\Socks\Server($loop); + $socket = new React\Socket\Server(1080, $loop); + $server->listen($socket); + + // new: authentication passed to server constructor + $server = new Clue\React\Socks\Server($loop, null, array( + 'tom' => 'password', + 'admin' => 'root' + )); + $server->listen($socket); + ``` + +* Feature: Improve error reporting for failed connections attempts by always including target URI in exceptions and + improve promise cancellation and clean up any garbage references. + (#82 and #83 by @clue) + + All error messages now always contain a reference to the remote URI to give + more details which connection actually failed and the reason for this error. + Similarly, any underlying connection issues to the proxy server will now be + reported as part of the previous exception. + + For most common use cases this means that simply reporting the `Exception` + message should give the most relevant details for any connection issues: + + ```php + $promise = $proxy->connect('tcp://example.com:80'); + $promise->then(function (ConnectionInterface $connection) { + // … + }, function (Exception $e) { + echo $e->getMessage(); + }); + ``` + +* Improve documentation and examples, link to other projects and update project homepage. + (#73, #75 and #85 by @clue) + +## 0.8.7 (2017-12-17) + +* Feature: Support SOCKS over TLS (`sockss://` URI scheme) + (#70 and #71 by @clue) + + ```php + // new: now supports SOCKS over TLS + $client = new Client('socks5s://localhost', $connector); + ``` + +* Feature: Support communication over Unix domain sockets (UDS) + (#69 by @clue) + + ```php + // new: now supports SOCKS over Unix domain sockets (UDS) + $client = new Client('socks5+unix:///tmp/proxy.sock', $connector); + ``` + +* Improve test suite by adding forward compatibility with PHPUnit 6 + (#68 by @clue) + +## 0.8.6 (2017-09-17) + +* Feature: Forward compatibility with Evenement v3.0 + (#67 by @WyriHaximus) + +## 0.8.5 (2017-09-01) + +* Feature: Use socket error codes for connection rejections + (#63 by @clue) + + ```php + $promise = $proxy->connect('imap.example.com:143'); + $promise->then(null, function (Exeption $e) { + if ($e->getCode() === SOCKET_EACCES) { + echo 'Failed to authenticate with proxy!'; + } + throw $e; + }); + ``` + +* Feature: Report matching SOCKS5 error codes for server side connection errors + (#62 by @clue) + +* Fix: Fix SOCKS5 client receiving destination hostnames and + fix IPv6 addresses as hostnames for TLS certificates + (#64 and #65 by @clue) + +* Improve test suite by locking Travis distro so new defaults will not break the build and + optionally exclude tests that rely on working internet connection + (#61 and #66 by @clue) + +## 0.8.4 (2017-07-27) + +* Feature: Server now passes client source address to Connector + (#60 by @clue) + +## 0.8.3 (2017-07-18) + +* Feature: Pass full remote URI as parameter to authentication callback + (#58 by @clue) + + ```php + // new third parameter passed to authentication callback + $server->setAuth(function ($user, $pass, $remote) { + $ip = parse_url($remote, PHP_URL_HOST); + + return ($ip === '127.0.0.1'); + }); + ``` + +* Fix: Fix connecting to IPv6 address via SOCKS5 server and validate target + URI so hostname can not contain excessive URI components + (#59 by @clue) + +* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors + (#57 by @clue) + +## 0.8.2 (2017-05-09) + +* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 + (#56 by @clue) + +## 0.8.1 (2017-04-21) + +* Update examples to use URIs with default port 1080 and accept proxy URI arguments + (#54 by @clue) + +* Remove now unneeded dependency on `react/stream` + (#55 by @clue) + +## 0.8.0 (2017-04-18) + +* Feature: Merge `Server` class from clue/socks-server + (#52 by @clue) + + ```php + $socket = new React\Socket\Server(1080, $loop); + $server = new Clue\React\Socks\Server($loop, $socket); + ``` + + > Upgrading from [clue/socks-server](https://github.com/clue/php-socks-server)? + The classes have been moved as-is, so you can simply start using the new + class name `Clue\React\Socks\Server` with no other changes required. + +## 0.7.0 (2017-04-14) + +* Feature / BC break: Replace depreacted SocketClient with Socket v0.7 and + use `connect($uri)` instead of `create($host, $port)` + (#51 by @clue) + + ```php + // old + $connector = new React\SocketClient\TcpConnector($loop); + $client = new Client(1080, $connector); + $client->create('google.com', 80)->then(function (Stream $conn) { + $conn->write("…"); + }); + + // new + $connector = new React\Socket\TcpConnector($loop); + $client = new Client(1080, $connector); + $client->connect('google.com:80')->then(function (ConnectionInterface $conn) { + $conn->write("…"); + }); + ``` + +* Improve test suite by adding PHPUnit to require-dev + (#50 by @clue) + +## 0.6.0 (2016-11-29) + +* Feature / BC break: Pass connector into `Client` instead of loop, remove unneeded deps + (#49 by @clue) + + ```php + // old (connector is create implicitly) + $client = new Client('127.0.0.1', $loop); + + // old (connector can optionally be passed) + $client = new Client('127.0.0.1', $loop, $connector); + + // new (connector is now mandatory) + $connector = new React\SocketClient\TcpConnector($loop); + $client = new Client('127.0.0.1', $connector); + ``` + +* Feature / BC break: `Client` now implements `ConnectorInterface`, remove `Connector` adapter + (#47 by @clue) + + ```php + // old (explicit connector functions as an adapter) + $connector = $client->createConnector(); + $promise = $connector->create('google.com', 80); + + // new (client can be used as connector right away) + $promise = $client->create('google.com', 80); + ``` + +* Feature / BC break: Remove `createSecureConnector()`, use `SecureConnector` instead + (#47 by @clue) + + ```php + // old (tight coupling and hidden dependency) + $tls = $client->createSecureConnector(); + $promise = $tls->create('google.com', 443); + + // new (more explicit, loose coupling) + $tls = new React\SocketClient\SecureConnector($client, $loop); + $promise = $tls->create('google.com', 443); + ``` + +* Feature / BC break: Remove `setResolveLocal()` and local DNS resolution and default to remote DNS resolution, use `DnsConnector` instead + (#44 by @clue) + + ```php + // old (implicitly defaults to true, can be disabled) + $client->setResolveLocal(false); + $tcp = $client->createConnector(); + $promise = $tcp->create('google.com', 80); + + // new (always disabled, can be re-enabled like this) + $factory = new React\Dns\Resolver\Factory(); + $resolver = $factory->createCached('8.8.8.8', $loop); + $tcp = new React\SocketClient\DnsConnector($client, $resolver); + $promise = $tcp->create('google.com', 80); + ``` + +* Feature / BC break: Remove `setTimeout()`, use `TimeoutConnector` instead + (#45 by @clue) + + ```php + // old (timeout only applies to TCP/IP connection) + $client = new Client('127.0.0.1', …); + $client->setTimeout(3.0); + $tcp = $client->createConnector(); + $promise = $tcp->create('google.com', 80); + + // new (timeout can be added to any layer) + $client = new Client('127.0.0.1', …); + $tcp = new React\SocketClient\TimeoutConnector($client, 3.0, $loop); + $promise = $tcp->create('google.com', 80); + ``` + +* Feature / BC break: Remove `setProtocolVersion()` and `setAuth()` mutators, only support SOCKS URI for protocol version and authentication (immutable API) + (#46 by @clue) + + ```php + // old (state can be mutated after instantiation) + $client = new Client('127.0.0.1', …); + $client->setProtocolVersion('5'); + $client->setAuth('user', 'pass'); + + // new (immutable after construction, already supported as of v0.5.2 - now mandatory) + $client = new Client('socks5://user:pass@127.0.0.1', …); + ``` + +## 0.5.2 (2016-11-25) + +* Feature: Apply protocol version and username/password auth from SOCKS URI + (#43 by @clue) + + ```php + // explicitly use SOCKS5 + $client = new Client('socks5://127.0.0.1', $loop); + + // use authentication (automatically SOCKS5) + $client = new Client('user:pass@127.0.0.1', $loop); + ``` + +* More explicit client examples, including proxy chaining + (#42 by @clue) + +## 0.5.1 (2016-11-21) + +* Feature: Support Promise cancellation + (#39 by @clue) + + ```php + $promise = $connector->create($host, $port); + + $promise->cancel(); + ``` + +* Feature: Timeout now cancels pending connection attempt + (#39, #22 by @clue) + +## 0.5.0 (2016-11-07) + +* Remove / BC break: Split off Server to clue/socks-server + (#35 by @clue) + + > Upgrading? Check [clue/socks-server](https://github.com/clue/php-socks-server) for details. + +* Improve documentation and project structure + +## 0.4.0 (2016-03-19) + +* Feature: Support proper SSL/TLS connections with additional SSL context options + (#31, #33 by @clue) + +* Documentation for advanced Connector setups (bindto, multihop) + (#32 by @clue) + +## 0.3.0 (2015-06-20) + +* BC break / Feature: Client ctor now accepts a SOCKS server URI + ([#24](https://github.com/clue/php-socks-react/pull/24)) + + ```php + // old + $client = new Client($loop, 'localhost', 9050); + + // new + $client = new Client('localhost:9050', $loop); + ``` + +* Feature: Automatically assume default SOCKS port (1080) if not given explicitly + ([#26](https://github.com/clue/php-socks-react/pull/26)) + +* Improve documentation and test suite + +## 0.2.1 (2014-11-13) + +* Support React PHP v0.4 (while preserving BC with React PHP v0.3) + ([#16](https://github.com/clue/php-socks-react/pull/16)) + +* Improve examples and add first class support for HHVM + ([#15](https://github.com/clue/php-socks-react/pull/15) and [#17](https://github.com/clue/php-socks-react/pull/17)) + +## 0.2.0 (2014-09-27) + +* BC break / Feature: Simplify constructors by making parameters optional. + ([#10](https://github.com/clue/php-socks-react/pull/10)) + + The `Factory` has been removed, you can now create instances of the `Client` + and `Server` yourself: + + ```php + // old + $factory = new Factory($loop, $dns); + $client = $factory->createClient('localhost', 9050); + $server = $factory->createSever($socket); + + // new + $client = new Client($loop, 'localhost', 9050); + $server = new Server($loop, $socket); + ``` + +* BC break: Remove HTTP support and link to [clue/buzz-react](https://github.com/clue/php-buzz-react) instead. + ([#9](https://github.com/clue/php-socks-react/pull/9)) + + HTTP operates on a different layer than this low-level SOCKS library. + Removing this reduces the footprint of this library. + + > Upgrading? Check the [README](https://github.com/clue/php-socks-react#http-requests) for details. + +* Fix: Refactored to support other, faster loops (libev/libevent) + ([#12](https://github.com/clue/php-socks-react/pull/12)) + +* Explicitly list dependencies, clean up examples and extend test suite significantly + +## 0.1.0 (2014-05-19) + +* First stable release +* Async SOCKS `Client` and `Server` implementation +* Project was originally part of [clue/socks](https://github.com/clue/php-socks) + and was split off from its latest releave v0.4.0 + ([#1](https://github.com/clue/reactphp-socks/issues/1)) + + > Upgrading from clue/socks v0.4.0? Use namespace `Clue\React\Socks` instead of `Socks` and you're ready to go! + +## 0.0.0 (2011-04-26) + +* Initial concept, originally tracked as part of + [clue/socks](https://github.com/clue/php-socks) diff --git a/vendor/clue/socks-react/LICENSE b/vendor/clue/socks-react/LICENSE new file mode 100644 index 0000000..8efa9aa --- /dev/null +++ b/vendor/clue/socks-react/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 Christian Lück + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/clue/socks-react/README.md b/vendor/clue/socks-react/README.md new file mode 100644 index 0000000..c886a23 --- /dev/null +++ b/vendor/clue/socks-react/README.md @@ -0,0 +1,1116 @@ +# clue/reactphp-socks + +[](https://github.com/clue/reactphp-socks/actions) +[](https://packagist.org/packages/clue/socks-react) + +Async SOCKS proxy connector client and server implementation, tunnel any TCP/IP-based +protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of +[ReactPHP](https://reactphp.org/). + +The SOCKS proxy protocol family (SOCKS5, SOCKS4 and SOCKS4a) is commonly used to +tunnel HTTP(S) traffic through an intermediary ("proxy"), to conceal the origin +address (anonymity) or to circumvent address blocking (geoblocking). While many +(public) SOCKS proxy servers often limit this to HTTP(S) port `80` and `443` +only, this can technically be used to tunnel any TCP/IP-based protocol (HTTP, +SMTP, IMAP etc.). +This library provides a simple API to create these tunneled connections for you. +Because it implements ReactPHP's standard +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface), +it can simply be used in place of a normal connector. +This makes it fairly simple to add SOCKS proxy support to pretty much any +existing higher-level protocol implementation. +Besides the client side, it also provides a simple SOCKS server implementation +which allows you to build your own SOCKS proxy servers with custom business logic. + +* **Async execution of connections** - + Send any number of SOCKS requests in parallel and process their + responses as soon as results come in. + The Promise-based design provides a *sane* interface to working with out of + order responses and possible connection errors. +* **Standard interfaces** - + Allows easy integration with existing higher-level components by implementing + ReactPHP's standard + [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface). +* **Lightweight, SOLID design** - + Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough) + and does not get in your way. + Builds on top of well-tested components and well-established concepts instead of reinventing the wheel. +* **Good test coverage** - + Comes with an automated tests suite and is regularly tested against actual proxy servers in the wild. + +**Table of contents** + +* [Support us](#support-us) +* [Quickstart example](#quickstart-example) +* [Usage](#usage) + * [Client](#client) + * [Plain TCP connections](#plain-tcp-connections) + * [Secure TLS connections](#secure-tls-connections) + * [HTTP requests](#http-requests) + * [Protocol version](#protocol-version) + * [DNS resolution](#dns-resolution) + * [Authentication](#authentication) + * [Proxy chaining](#proxy-chaining) + * [Connection timeout](#connection-timeout) + * [SOCKS over TLS](#socks-over-tls) + * [Unix domain sockets](#unix-domain-sockets) + * [Server](#server) + * [Server connector](#server-connector) + * [Authentication](#server-authentication) + * [Proxy chaining](#server-proxy-chaining) + * [SOCKS over TLS](#server-socks-over-tls) + * [Unix domain sockets](#server-unix-domain-sockets) +* [Servers](#servers) + * [Using a PHP SOCKS server](#using-a-php-socks-server) + * [Using SSH as a SOCKS server](#using-ssh-as-a-socks-server) + * [Using the Tor (anonymity network) to tunnel SOCKS connections](#using-the-tor-anonymity-network-to-tunnel-socks-connections) +* [Install](#install) +* [Tests](#tests) +* [License](#license) +* [More](#more) + +## Support us + +We invest a lot of time developing, maintaining and updating our awesome +open-source projects. You can help us sustain this high-quality of our work by +[becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get +numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) +for details. + +Let's take these projects to the next level together! 🚀 + +## Quickstart example + +Once [installed](#install), you can use the following code to send a secure +HTTPS request to google.com through a local SOCKS proxy server: + +```php +<?php + +require __DIR__ . '/vendor/autoload.php'; + +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($connector); + +$browser->get('https://google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string) $response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +If you're not already running any other [SOCKS proxy server](#servers), +you can use the following code to create a SOCKS +proxy server listening for connections on `127.0.0.1:1080`: + +```php +<?php + +require __DIR__ . '/vendor/autoload.php'; + +// start a new SOCKS proxy server +$socks = new Clue\React\Socks\Server(); + +// listen on 127.0.0.1:1080 +$socket = new React\Socket\SocketServer('127.0.0.1:1080'); +$socks->listen($socket); +``` + +See also the [examples](examples). + +## Usage + +### Client + +The `Client` is responsible for communication with your SOCKS server instance. + +Its constructor simply accepts a SOCKS proxy URI with the SOCKS proxy server address: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); +``` + +You can omit the port if you're using the default SOCKS port 1080: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1'); +``` + +If you need custom connector settings (DNS resolution, TLS parameters, timeouts, +proxy servers etc.), you can explicitly pass a custom instance of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + +```php +$connector = new React\Socket\Connector(array( + 'dns' => '127.0.0.1', + 'tcp' => array( + 'bindto' => '192.168.10.1:0' + ) +)); + +$proxy = new Clue\React\Socks\Client('my-socks-server.local:1080', $connector); +``` + +This is one of the two main classes in this package. +Because it implements ReactPHP's standard +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface), +it can simply be used in place of a normal connector. +Accordingly, it provides only a single public method, the +[`connect()`](https://github.com/reactphp/socket#connect) method. +The `connect(string $uri): PromiseInterface<ConnectionInterface, Exception>` +method can be used to establish a streaming connection. +It returns a [Promise](https://github.com/reactphp/promise) which either +fulfills with a [ConnectionInterface](https://github.com/reactphp/socket#connectioninterface) +on success or rejects with an `Exception` on error. + +This makes it fairly simple to add SOCKS proxy support to pretty much any +higher-level component: + +```diff +- $acme = new AcmeApi($connector); ++ $proxy = new Clue\React\Socks\Client('127.0.0.1:1080', $connector); ++ $acme = new AcmeApi($proxy); +``` + +#### Plain TCP connections + +SOCKS proxies are most frequently used to issue HTTP(S) requests to your destination. +However, this is actually performed on a higher protocol layer and this +connector is actually inherently a general-purpose plain TCP/IP connector. +As documented above, you can simply invoke its `connect()` method to establish +a streaming plain TCP/IP connection and use any higher level protocol like so: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + echo 'connected to www.google.com:80'; + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +You can either use the `Client` directly or you may want to wrap this connector +in ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector): + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$connector->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + echo 'connected to www.google.com:80'; + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +See also the [first example](examples). + +The `tcp://` scheme can also be omitted. +Passing any other scheme will reject the promise. + +Pending connection attempts can be cancelled by cancelling its pending promise like so: + +```php +$promise = $connector->connect($uri); + +$promise->cancel(); +``` + +Calling `cancel()` on a pending promise will cancel the underlying TCP/IP +connection to the SOCKS server and/or the SOCKS protocol negotiation and reject +the resulting promise. + +#### Secure TLS connections + +This class can also be used if you want to establish a secure TLS connection +(formerly known as SSL) between you and your destination, such as when using +secure HTTPS to your destination site. You can simply wrap this connector in +ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector): + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$connector->connect('tls://www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + // proceed with just the plain text data + // everything is encrypted/decrypted automatically + echo 'connected to SSL encrypted www.google.com'; + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +See also the [second example](examples). + +Pending connection attempts can be cancelled by canceling its pending promise +as usual. + +> Note how secure TLS connections are in fact entirely handled outside of + this SOCKS client implementation. + +You can optionally pass additional +[SSL context options](http://php.net/manual/en/context.ssl.php) +to the constructor like this: + +```php +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'tls' => array( + 'verify_peer' => false, + 'verify_peer_name' => false + ), + 'dns' => false +)); +``` + +#### HTTP requests + +This library also allows you to send +[HTTP requests through a SOCKS proxy server](https://github.com/reactphp/http#socks-proxy). + +In order to send HTTP requests, you first have to add a dependency for +[ReactPHP's async HTTP client](https://github.com/reactphp/http#client-usage). +This allows you to send both plain HTTP and TLS-encrypted HTTPS requests like this: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); + +$browser = new React\Http\Browser($connector); + +$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { + var_dump($response->getHeaders(), (string) $response->getBody()); +}, function (Exception $e) { + echo 'Error: ' . $e->getMessage() . PHP_EOL; +}); +``` + +See also [ReactPHP's HTTP client](https://github.com/reactphp/http#client-usage) +and any of the [examples](examples) for more details. + +#### Protocol version + +This library supports the SOCKS5 and SOCKS4(a) protocol versions. +It focuses on the most commonly used core feature of connecting to a destination +host through the SOCKS proxy server. In this mode, a SOCKS proxy server acts as +a generic proxy allowing higher level application protocols to work through it. + +<table> + <tr> + <th></th> + <th>SOCKS5</th> + <th>SOCKS4(a)</th> + </tr> + <tr> + <th>Protocol specification</th> + <td><a href="https://tools.ietf.org/html/rfc1928">RFC 1928</a></td> + <td> + <a href="https://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4.protocol">SOCKS4.protocol</a> / + <a href="https://ftp.icm.edu.pl/packages/socks/socks4/SOCKS4A.protocol">SOCKS4A.protocol</a> + </td> + </tr> + <tr> + <th>Tunnel outgoing TCP/IP connections</th> + <td>✓</td> + <td>✓</td> + </tr> + <tr> + <th><a href="#dns-resolution">Remote DNS resolution</a></th> + <td>✓</td> + <td>✗ / ✓</td> + </tr> + <tr> + <th>IPv6 addresses</th> + <td>✓</td> + <td>✗</td> + </tr> + <tr> + <th><a href="#authentication">Username/Password authentication</a></th> + <td>✓ (as per <a href="https://tools.ietf.org/html/rfc1929">RFC 1929</a>)</td> + <td>✗</td> + </tr> + <tr> + <th>Handshake # roundtrips</th> + <td>2 (3 with authentication)</td> + <td>1</td> + </tr> + <tr> + <th>Handshake traffic<br />+ remote DNS</th> + <td><em>variable</em> (+ auth + IPv6)<br />+ hostname - 3</td> + <td>17 bytes<br />+ hostname + 1</td> + </tr> + <tr> + <th>Incoming BIND requests</th> + <td><em>not implemented</em></td> + <td><em>not implemented</em></td> + </tr> + <tr> + <th>UDP datagrams</th> + <td><em>not implemented</em></td> + <td>✗</td> + </tr> + <tr> + <th>GSSAPI authentication</th> + <td><em>not implemented</em></td> + <td>✗</td> + </tr> +</table> + +By default, the `Client` communicates via SOCKS5 with the SOCKS server. +This is done because SOCKS5 is the latest version from the SOCKS protocol family +and generally has best support across other vendors. +You can also omit the default `socks://` URI scheme. Similarly, the `socks5://` +URI scheme acts as an alias for the default `socks://` URI scheme. + +```php +// all three forms are equivalent +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); +$proxy = new Clue\React\Socks\Client('socks://127.0.0.1:1080'); +$proxy = new Clue\React\Socks\Client('socks5://127.0.0.1:1080'); +``` + +If want to explicitly set the protocol version to SOCKS4(a), you can use the URI +scheme `socks4://` as part of the SOCKS URI: + +```php +$proxy = new Clue\React\Socks\Client('socks4://127.0.0.1:1080'); +``` + +#### DNS resolution + +By default, the `Client` does not perform any DNS resolution at all and simply +forwards any hostname you're trying to connect to to the SOCKS server. +The remote SOCKS server is thus responsible for looking up any hostnames via DNS +(this default mode is thus called *remote DNS resolution*). +As seen above, this mode is supported by the SOCKS5 and SOCKS4a protocols, but +not the original SOCKS4 protocol, as the protocol lacks a way to communicate hostnames. + +On the other hand, all SOCKS protocol versions support sending destination IP +addresses to the SOCKS server. +In this mode you either have to stick to using IPs only (which is ofen unfeasable) +or perform any DNS lookups locally and only transmit the resolved destination IPs +(this mode is thus called *local DNS resolution*). + +The default *remote DNS resolution* is useful if your local `Client` either can +not resolve target hostnames because it has no direct access to the internet or +if it should not resolve target hostnames because its outgoing DNS traffic might +be intercepted (in particular when using the +[Tor network](#using-the-tor-anonymity-network-to-tunnel-socks-connections)). + +As noted above, the `Client` defaults to using remote DNS resolution. +However, wrapping the `Client` in ReactPHP's +[`Connector`](https://github.com/reactphp/socket#connector) actually +performs local DNS resolution unless explicitly defined otherwise. +Given that remote DNS resolution is assumed to be the preferred mode, all +other examples explicitly disable DNS resolution like this: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false +)); +``` + +If you want to explicitly use *local DNS resolution* (such as when explicitly +using SOCKS4), you can use the following code: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +// set up Connector which uses Google's public DNS (8.8.8.8) +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => '8.8.8.8' +)); +``` + +See also the [fourth example](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise +as usual. + +> Note how local DNS resolution is in fact entirely handled outside of this + SOCKS client implementation. + +#### Authentication + +This library supports username/password authentication for SOCKS5 servers as +defined in [RFC 1929](https://tools.ietf.org/html/rfc1929). + +On the client side, simply pass your username and password to use for +authentication (see below). +For each further connection the client will merely send a flag to the server +indicating authentication information is available. +Only if the server requests authentication during the initial handshake, +the actual authentication credentials will be transmitted to the server. + +Note that the password is transmitted in cleartext to the SOCKS proxy server, +so this methods should not be used on a network where you have to worry about eavesdropping. + +You can simply pass the authentication information as part of the SOCKS URI: + +```php +$proxy = new Clue\React\Socks\Client('alice:password@127.0.0.1:1080'); +``` + +Note that both the username and password must be percent-encoded if they contain +special characters: + +```php +$user = 'he:llo'; +$pass = 'p@ss'; +$url = rawurlencode($user) . ':' . rawurlencode($pass) . '@127.0.0.1:1080'; + +$proxy = new Clue\React\Socks\Client($url); +``` + +> The authentication details will be transmitted in cleartext to the SOCKS proxy + server only if it requires username/password authentication. + If the authentication details are missing or not accepted by the remote SOCKS + proxy server, it is expected to reject each connection attempt with an + exception error code of `SOCKET_EACCES` (13). + +Authentication is only supported by protocol version 5 (SOCKS5), +so passing authentication to the `Client` enforces communication with protocol +version 5 and complains if you have explicitly set anything else: + +```php +// throws InvalidArgumentException +new Clue\React\Socks\Client('socks4://alice:password@127.0.0.1:1080'); +``` + +#### Proxy chaining + +The `Client` is responsible for creating connections to the SOCKS server which +then connects to the target host. + +``` +Client -> SocksServer -> TargetHost +``` + +Sometimes it may be required to establish outgoing connections via another SOCKS +server. +For example, this can be useful if you want to conceal your origin address. + +``` +Client -> MiddlemanSocksServer -> TargetSocksServer -> TargetHost +``` + +The `Client` uses any instance of the `ConnectorInterface` to establish +outgoing connections. +In order to connect through another SOCKS server, you can simply use another +SOCKS connector from another SOCKS client like this: + +```php +// https via the proxy chain "MiddlemanSocksServer -> TargetSocksServer -> TargetHost" +// please note how the client uses TargetSocksServer (not MiddlemanSocksServer!), +// which in turn then uses MiddlemanSocksServer. +// this creates a TCP/IP connection to MiddlemanSocksServer, which then connects +// to TargetSocksServer, which then connects to the TargetHost +$middle = new Clue\React\Socks\Client('127.0.0.1:1080'); +$target = new Clue\React\Socks\Client('example.com:1080', $middle); + +$connector = new React\Socket\Connector(array( + 'tcp' => $target, + 'dns' => false +)); + +$connector->connect('tls://www.google.com:443')->then(function (React\Socket\ConnectionInterface $connection) { + // … +}); +``` + +See also the [third example](examples). + +Pending connection attempts can be canceled by canceling its pending promise +as usual. + +Proxy chaining can happen on the server side and/or the client side: + +* If you ask your client to chain through multiple proxies, then each proxy + server does not really know anything about chaining at all. + This means that this is a client-only property. + +* If you ask your server to chain through another proxy, then your client does + not really know anything about chaining at all. + This means that this is a server-only property and not part of this class. + For example, you can find this in the below [`Server`](#server-proxy-chaining) + class or somewhat similar when you're using the + [Tor network](#using-the-tor-anonymity-network-to-tunnel-socks-connections). + +#### Connection timeout + +By default, the `Client` does not implement any timeouts for establishing remote +connections. +Your underlying operating system may impose limits on pending and/or idle TCP/IP +connections, anywhere in a range of a few minutes to several hours. + +Many use cases require more control over the timeout and likely values much +smaller, usually in the range of a few seconds only. + +You can use ReactPHP's [`Connector`](https://github.com/reactphp/socket#connector) +to decorate any given `ConnectorInterface` instance. +It provides the same `connect()` method, but will automatically reject the +underlying connection attempt if it takes too long: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$connector = new React\Socket\Connector(array( + 'tcp' => $proxy, + 'dns' => false, + 'timeout' => 3.0 +)); + +$connector->connect('tcp://google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + // connection succeeded within 3.0 seconds +}); +``` + +See also any of the [examples](examples). + +Pending connection attempts can be cancelled by cancelling its pending promise +as usual. + +> Note how connection timeout is in fact entirely handled outside of this + SOCKS client implementation. + +#### SOCKS over TLS + +All [SOCKS protocol versions](#protocol-version) support forwarding TCP/IP +based connections and higher level protocols. +This implies that you can also use [secure TLS connections](#secure-tls-connections) +to transfer sensitive data across SOCKS proxy servers. +This means that no eavesdropper nor the proxy server will be able to decrypt +your data. + +However, the initial SOCKS communication between the client and the proxy is +usually via an unencrypted, plain TCP/IP connection. +This means that an eavesdropper may be able to see *where* you connect to and +may also be able to see your [SOCKS authentication](#authentication) details +in cleartext. + +As an alternative, you may establish a secure TLS connection to your SOCKS +proxy before starting the initial SOCKS communication. +This means that no eavesdroppper will be able to see the destination address +you want to connect to or your [SOCKS authentication](#authentication) details. + +You can use the `sockss://` URI scheme or use an explicit +[SOCKS protocol version](#protocol-version) like this: + +```php +$proxy = new Clue\React\Socks\Client('sockss://127.0.0.1:1080'); + +$proxy = new Clue\React\Socks\Client('socks4s://127.0.0.1:1080'); +``` + +See also [example 32](examples). + +Similarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$proxy = new Clue\React\Socks\Client('sockss://alice:password@127.0.0.1:1080'); +``` + +> Note that for most use cases, [secure TLS connections](#secure-tls-connections) + should be used instead. SOCKS over TLS is considered advanced usage and is + used very rarely in practice. + In particular, the SOCKS server has to accept secure TLS connections, see + also [Server SOCKS over TLS](#server-socks-over-tls) for more details. + Also, PHP does not support "double encryption" over a single connection. + This means that enabling [secure TLS connections](#secure-tls-connections) + over a communication channel that has been opened with SOCKS over TLS + may not be supported. + +> Note that the SOCKS protocol does not support the notion of TLS. The above + works reasonably well because TLS is only used for the connection between + client and proxy server and the SOCKS protocol data is otherwise identical. + This implies that this may also have only limited support for + [proxy chaining](#proxy-chaining) over multiple TLS paths. + +#### Unix domain sockets + +All [SOCKS protocol versions](#protocol-version) support forwarding TCP/IP +based connections and higher level protocols. +In some advanced cases, it may be useful to let your SOCKS server listen on a +Unix domain socket (UDS) path instead of a IP:port combination. +For example, this allows you to rely on file system permissions instead of +having to rely on explicit [authentication](#authentication). + +You can use the `socks+unix://` URI scheme or use an explicit +[SOCKS protocol version](#protocol-version) like this: + +```php +$proxy = new Clue\React\Socks\Client('socks+unix:///tmp/proxy.sock'); + +$proxy = new Clue\React\Socks\Client('socks4+unix:///tmp/proxy.sock'); +``` + +Similarly, you can also combine this with [authentication](#authentication) +like this: + +```php +$proxy = new Clue\React\Socks\Client('socks+unix://alice:password@/tmp/proxy.sock'); +``` + +> Note that Unix domain sockets (UDS) are considered advanced usage and PHP only + has limited support for this. + In particular, enabling [secure TLS](#secure-tls-connections) may not be + supported. + +> Note that the SOCKS protocol does not support the notion of UDS paths. The above + works reasonably well because UDS is only used for the connection between + client and proxy server and the path will not actually passed over the protocol. + This implies that this does also not support [proxy chaining](#proxy-chaining) + over multiple UDS paths. + +### Server + +The `Server` is responsible for accepting incoming communication from SOCKS clients +and forwarding the requested connection to the target host. +It supports the SOCKS5 and SOCKS4(a) protocol versions by default. +You can start listening on an underlying TCP/IP socket server like this: + +```php +$socks = new Clue\React\Socks\Server(); + +// listen on 127.0.0.1:1080 +$socket = new React\Socket\SocketServer('127.0.0.1:1080'); +$socks->listen($socket); +``` + +This class takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use for this object. You can use a `null` value +here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). +This value SHOULD NOT be given unless you're sure you want to explicitly use a +given event loop instance. + +Additionally, the `Server` constructor accepts optional parameters to explicitly +configure the [connector](#server-connector) to use and to require +[authentication](#server-authentication). For more details, read on... + +#### Server connector + +The `Server` uses an instance of ReactPHP's +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) +to establish outgoing connections for each incoming connection request. + +If you need custom connector settings (DNS resolution, TLS parameters, timeouts, +proxy servers etc.), you can explicitly pass a custom instance of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): + +```php +$connector = new React\Socket\Connector(array( + 'dns' => '127.0.0.1', + 'tcp' => array( + 'bindto' => '192.168.10.1:0' + ) +)); + +$socks = new Clue\React\Socks\Server(null, $connector); +``` + +If you want to forward the outgoing connection through another SOCKS proxy, you +may also pass a [`Client`](#client) instance as a connector, see also +[server proxy chaining](#server-proxy-chaining) for more details. + +Internally, the `Server` uses ReactPHP's normal +[`connect()`](https://github.com/reactphp/socket#connect) method, but +it also passes the original client IP as the `?source={remote}` parameter. +The `source` parameter contains the full remote URI, including the protocol +and any authentication details, for example `socks://alice:password@1.2.3.4:5678` +or `socks4://1.2.3.4:5678` for legacy SOCKS4(a). +You can use this parameter for logging purposes or to restrict connection +requests for certain clients by providing a custom implementation of the +[`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface). + +#### Server authentication + +By default, the `Server` does not require any authentication from the clients. +You can enable authentication support so that clients need to pass a valid +username and password before forwarding any connections. + +Setting authentication on the `Server` enforces each further connected client +to use protocol version 5 (SOCKS5). +If a client tries to use any other protocol version, does not send along +authentication details or if authentication details can not be verified, +the connection will be rejected. + +If you only want to accept static authentication details, you can simply pass an +additional assoc array with your authentication details to the `Server` like this: + +```php +$socks = new Clue\React\Socks\Server(null, null, array( + 'alice' => 'password', + 'bob' => 's3cret!1' +)); +``` + +See also [example #12](examples). + +If you want more control over authentication, you can pass an authenticator +function that should return a `bool` value like this synchronous example: + +```php +$socks = new Clue\React\Socks\Server(null, null, function ($username, $password, $remote) { + // $remote is a full URI à la socks://alice:password@192.168.1.1:1234 + // or sockss://alice:password@192.168.1.1:1234 for SOCKS over TLS + // or may be null when remote is unknown (SOCKS over Unix Domain Sockets) + // useful for logging or extracting parts, such as the remote IP + $ip = parse_url($remote, PHP_URL_HOST); + + return ($username === 'root' && $password === 'secret' && $ip === '127.0.0.1'); +}); +``` + +Because your authentication mechanism might take some time to actually check the +provided authentication credentials (like querying a remote database or webservice), +the server also supports a [Promise](https://github.com/reactphp/promise)-based +interface. While this might seem more complex at first, it actually provides a +very powerful way of handling a large number of connections concurrently without +ever blocking any connections. You can return a [Promise](https://github.com/reactphp/promise) +from the authenticator function that will fulfill with a `bool` value like this +async example: + +```php +$socks = new Clue\React\Socks\Server(null, null, function ($username, $password) use ($db) { + // pseudo-code: query database for given authentication details + return $db->query( + 'SELECT 1 FROM users WHERE name = ? AND password = ?', + array($username, $password) + )->then(function (QueryResult $result) { + // ensure we find exactly one match in the database + return count($result->resultRows) === 1; + }); +}); +``` + +#### Server proxy chaining + +The `Server` is responsible for creating connections to the target host. + +``` +Client -> SocksServer -> TargetHost +``` + +Sometimes it may be required to establish outgoing connections via another SOCKS +server. +For example, this can be useful if your target SOCKS server requires +authentication, but your client does not support sending authentication +information (e.g. like most webbrowser). + +``` +Client -> MiddlemanSocksServer -> TargetSocksServer -> TargetHost +``` + +The `Server` uses any instance of the `ConnectorInterface` to establish outgoing +connections. +In order to connect through another SOCKS server, you can simply use the +[`Client`](#client) SOCKS connector from above. +You can create a SOCKS `Client` instance like this: + +```php +// set next SOCKS server example.com:1080 as target +$proxy = new Clue\React\Socks\Client('alice:password@example.com:1080'); + +// start a new server which forwards all connections to the other SOCKS server +$socks = new Clue\React\Socks\Server(null, $proxy); + +// listen on 127.0.0.1:1080 +$socket = new React\Socket\SocketServer('127.0.0.1:1080'); +$socks->listen($socket); +``` + +See also [example #21](examples). + +Proxy chaining can happen on the server side and/or the client side: + +* If you ask your client to chain through multiple proxies, then each proxy + server does not really know anything about chaining at all. + This means that this is a client-only property and not part of this class. + For example, you can find this in the above [`Client`](#proxy-chaining) class. + +* If you ask your server to chain through another proxy, then your client does + not really know anything about chaining at all. + This means that this is a server-only property and can be implemented as above. + +#### Server SOCKS over TLS + +Both SOCKS5 and SOCKS4(a) protocol versions support forwarding TCP/IP based +connections and higher level protocols. +This implies that you can also use [secure TLS connections](#secure-tls-connections) +to transfer sensitive data across SOCKS proxy servers. +This means that no eavesdropper nor the proxy server will be able to decrypt +your data. + +However, the initial SOCKS communication between the client and the proxy is +usually via an unencrypted, plain TCP/IP connection. +This means that an eavesdropper may be able to see *where* the client connects +to and may also be able to see the [SOCKS authentication](#authentication) +details in cleartext. + +As an alternative, you may listen for SOCKS over TLS connections so +that the client has to establish a secure TLS connection to your SOCKS +proxy before starting the initial SOCKS communication. +This means that no eavesdroppper will be able to see the destination address +the client wants to connect to or their [SOCKS authentication](#authentication) +details. + +You can simply start your listening socket on the `tls://` URI scheme like this: + +```php +$socks = new Clue\React\Socks\Server(); + +// listen on tls://127.0.0.1:1080 with the given server certificate +$socket = new React\Socket\SocketServer('tls://127.0.0.1:1080', array( + 'tls' => array( + 'local_cert' => __DIR__ . '/localhost.pem', + ) +)); +$socks->listen($socket); +``` + +See also [example 31](examples). + +> Note that for most use cases, [secure TLS connections](#secure-tls-connections) + should be used instead. SOCKS over TLS is considered advanced usage and is + used very rarely in practice. + +> Note that the SOCKS protocol does not support the notion of TLS. The above + works reasonably well because TLS is only used for the connection between + client and proxy server and the SOCKS protocol data is otherwise identical. + This implies that this does also not support [proxy chaining](#server-proxy-chaining) + over multiple TLS paths. + +#### Server Unix domain sockets + +Both SOCKS5 and SOCKS4(a) protocol versions support forwarding TCP/IP based +connections and higher level protocols. +In some advanced cases, it may be useful to let your SOCKS server listen on a +Unix domain socket (UDS) path instead of a IP:port combination. +For example, this allows you to rely on file system permissions instead of +having to rely on explicit [authentication](#server-authentication). + +You can simply start your listening socket on the `unix://` URI scheme like this: + +```php +$socks = new Clue\React\Socks\Server(); + +// listen on /tmp/proxy.sock +$socket = new React\Socket\SocketServer('unix:///tmp/proxy.sock'); +$socks->listen($socket); +``` + +> Note that Unix domain sockets (UDS) are considered advanced usage and that + the SOCKS protocol does not support the notion of UDS paths. The above + works reasonably well because UDS is only used for the connection between + client and proxy server and the path will not actually passed over the protocol. + This implies that this does also not support [proxy chaining](#server-proxy-chaining) + over multiple UDS paths. + +## Servers + +### Using a PHP SOCKS server + +* If you're looking for an end-user SOCKS server daemon, you may want to use + [LeProxy](https://leproxy.org/) or [clue/psocksd](https://github.com/clue/psocksd). +* If you're looking for a SOCKS server implementation, consider using + the above [`Server`](#server) class. + +### Using SSH as a SOCKS server + +If you already have an SSH server set up, you can easily use it as a SOCKS +tunnel end point. On your client, simply start your SSH client and use +the `-D <port>` option to start a local SOCKS server (quoting the man page: +a `local "dynamic" application-level port forwarding`). + +You can start a local SOCKS server by creating a loopback connection to your +local system if you already run an SSH daemon: + +```bash +ssh -D 1080 localhost +``` + +Alternatively, you can start a local SOCKS server tunneling through a given +remote host that runs an SSH daemon: + +```bash +ssh -D 1080 example.com +``` + +Now you can simply use this SSH SOCKS server like this: + +```PHP +$proxy = new Clue\React\Socks\Client('127.0.0.1:1080'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +Note that the above will allow all users on the local system to connect over +your SOCKS server without authentication which may or may not be what you need. +As an alternative, recent OpenSSH client versions also support +[Unix domain sockets](#unix-domain-sockets) (UDS) paths so that you can rely +on Unix file system permissions instead: + +```bash +ssh -D/tmp/proxy.sock example.com +``` + +Now you can simply use this SSH SOCKS server like this: + +```PHP +$proxy = new Clue\React\Socks\Client('socks+unix:///tmp/proxy.sock'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +> As an alternative to requiring this manual setup, you may also want to look + into using [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy) + which automatically creates this SSH tunnel for you. It provides an implementation of the same + [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + so that supporting either proxy protocol should be fairly trivial. + +### Using the Tor (anonymity network) to tunnel SOCKS connections + +The [Tor anonymity network](https://www.torproject.org/) client software is designed +to encrypt your traffic and route it over a network of several nodes to conceal its origin. +It presents a SOCKS5 and SOCKS4(a) interface on TCP port 9050 by default +which allows you to tunnel any traffic through the anonymity network: + +```php +$proxy = new Clue\React\Socks\Client('127.0.0.1:9050'); + +$proxy->connect('tcp://www.google.com:80')->then(function (React\Socket\ConnectionInterface $connection) { + $connection->write("GET / HTTP/1.0\r\n\r\n"); + + $connection->on('data', function ($chunk) { + echo $chunk; + }); +}); +``` + +In most common scenarios you probably want to stick to default +[remote DNS resolution](#dns-resolution) and don't want your client to resolve the target hostnames, +because you would leak DNS information to anybody observing your local traffic. +Also, Tor provides hidden services through an `.onion` pseudo top-level domain +which have to be resolved by Tor. + +## Install + +The recommended way to install this library is [through Composer](https://getcomposer.org/). +[New to Composer?](https://getcomposer.org/doc/00-intro.md) + +This project follows [SemVer](https://semver.org/). +This will install the latest supported version: + +```bash +composer require clue/socks-react:^1.4 +``` + +See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. + +This project aims to run on any platform and thus does not require any PHP +extensions and supports running on legacy PHP 5.3 through current PHP 8+ and +HHVM. +It's *highly recommended to use the latest supported PHP version* for this project. + +## Tests + +To run the test suite, you first need to clone this repo and then install all +dependencies [through Composer](https://getcomposer.org/): + +```bash +composer install +``` + +To run the test suite, go to the project root and run: + +```bash +vendor/bin/phpunit +``` + +The test suite contains a number of tests that rely on a working internet +connection, alternatively you can also run it like this: + +```bash +vendor/bin/phpunit --exclude-group internet +``` + +## License + +This project is released under the permissive [MIT license](LICENSE). + +> Did you know that I offer custom development services and issuing invoices for + sponsorships of releases and for contributions? Contact me (@clue) for details. + +## More + +* If you want to learn more about how the + [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + and its usual implementations look like, refer to the documentation of the + underlying [react/socket component](https://github.com/reactphp/socket). +* If you want to learn more about processing streams of data, refer to the + documentation of the underlying + [react/stream](https://github.com/reactphp/stream) component. +* As an alternative to a SOCKS5 / SOCKS4(a) proxy, you may also want to look into + using an HTTP CONNECT proxy instead. + You may want to use [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-proxy) + which also provides an implementation of the same + [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + so that supporting either proxy protocol should be fairly trivial. +* As an alternative to a SOCKS5 / SOCKS4(a) proxy, you may also want to look into + using an SSH proxy (SSH tunnel) instead. + You may want to use [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy) + which also provides an implementation of the same + [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface) + so that supporting either proxy protocol should be fairly trivial. +* If you're dealing with public proxies, you'll likely have to work with mixed + quality and unreliable proxies. You may want to look into using + [clue/reactphp-connection-manager-extra](https://github.com/clue/reactphp-connection-manager-extra) + which allows retrying unreliable ones, implying connection timeouts, + concurrently working with multiple connectors and more. +* If you're looking for an end-user SOCKS server daemon, you may want to use + [LeProxy](https://leproxy.org/) or [clue/psocksd](https://github.com/clue/psocksd). diff --git a/vendor/clue/socks-react/composer.json b/vendor/clue/socks-react/composer.json new file mode 100644 index 0000000..2a26a7a --- /dev/null +++ b/vendor/clue/socks-react/composer.json @@ -0,0 +1,31 @@ +{ + "name": "clue/socks-react", + "description": "Async SOCKS proxy connector client and server implementation, tunnel any TCP/IP-based protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of ReactPHP.", + "keywords": ["socks client", "socks server", "socks5", "socks4a", "proxy server", "tcp tunnel", "async", "ReactPHP"], + "homepage": "https://github.com/clue/reactphp-socks", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "require": { + "php": ">=5.3", + "react/promise": "^3 || ^2.1 || ^1.2", + "react/socket": "^1.12" + }, + "require-dev": { + "clue/block-react": "^1.5", + "clue/connection-manager-extra": "^1.3", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2", + "react/http": "^1.6" + }, + "autoload": { + "psr-4": { "Clue\\React\\Socks\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Socks\\": "tests/" } + } +} diff --git a/vendor/clue/socks-react/src/Client.php b/vendor/clue/socks-react/src/Client.php new file mode 100644 index 0000000..5645c25 --- /dev/null +++ b/vendor/clue/socks-react/src/Client.php @@ -0,0 +1,458 @@ +<?php + +namespace Clue\React\Socks; + +use React\Promise; +use React\Promise\PromiseInterface; +use React\Promise\Deferred; +use React\Socket\ConnectionInterface; +use React\Socket\Connector; +use React\Socket\ConnectorInterface; +use React\Socket\FixedUriConnector; +use Exception; +use InvalidArgumentException; +use RuntimeException; + +final class Client implements ConnectorInterface +{ + /** + * @var ConnectorInterface + */ + private $connector; + + private $socksUri; + + private $protocolVersion = 5; + + private $auth = null; + + /** + * @param string $socksUri + * @param ?ConnectorInterface $connector + * @throws InvalidArgumentException + */ + public function __construct( + #[\SensitiveParameter] + $socksUri, + ConnectorInterface $connector = null + ) { + // support `sockss://` scheme for SOCKS over TLS + // support `socks+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^(socks(?:5|4)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) { + // rewrite URI to parse SOCKS scheme, authentication and dummy host + $socksUri = $match[1] . '://' . $match[3] . 'localhost'; + + // connector uses appropriate transport scheme and explicit host given + $connector = new FixedUriConnector( + ($match[2] === 's' ? 'tls://' : 'unix://') . $match[4], + $connector ?: new Connector() + ); + } + + // assume default scheme if none is given + if (strpos($socksUri, '://') === false) { + $socksUri = 'socks://' . $socksUri; + } + + // parse URI into individual parts + $parts = parse_url($socksUri); + if (!$parts || !isset($parts['scheme'], $parts['host'])) { + throw new InvalidArgumentException('Invalid SOCKS server URI "' . $socksUri . '"'); + } + + // assume default port + if (!isset($parts['port'])) { + $parts['port'] = 1080; + } + + // user or password in URI => SOCKS5 authentication + if (isset($parts['user']) || isset($parts['pass'])) { + if ($parts['scheme'] !== 'socks' && $parts['scheme'] !== 'socks5') { + // fail if any other protocol version given explicitly + throw new InvalidArgumentException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication'); + } + $parts += array('user' => '', 'pass' => ''); + $this->setAuth(rawurldecode($parts['user']), rawurldecode($parts['pass'])); + } + + // check for valid protocol version from URI scheme + $this->setProtocolVersionFromScheme($parts['scheme']); + + $this->socksUri = $parts['host'] . ':' . $parts['port']; + $this->connector = $connector ?: new Connector(); + } + + private function setProtocolVersionFromScheme($scheme) + { + if ($scheme === 'socks' || $scheme === 'socks5') { + $this->protocolVersion = 5; + } elseif ($scheme === 'socks4') { + $this->protocolVersion = 4; + } else { + throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"'); + } + } + + /** + * set login data for username/password authentication method (RFC1929) + * + * @param string $username + * @param string $password + * @link http://tools.ietf.org/html/rfc1929 + */ + private function setAuth( + $username, + #[\SensitiveParameter] + $password + ) { + if (strlen($username) > 255 || strlen($password) > 255) { + throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each'); + } + $this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password; + } + + /** + * Establish a TCP/IP connection to the given target URI through the SOCKS server + * + * Many higher-level networking protocols build on top of TCP. It you're dealing + * with one such client implementation, it probably uses/accepts an instance + * implementing ReactPHP's `ConnectorInterface` (and usually its default `Connector` + * instance). In this case you can also pass this `Connector` instance instead + * to make this client implementation SOCKS-aware. That's it. + * + * @param string $uri + * @return PromiseInterface Promise<ConnectionInterface,Exception> + */ + public function connect($uri) + { + if (strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + $parts = parse_url($uri); + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { + return Promise\reject(new InvalidArgumentException('Invalid target URI specified')); + } + + $host = trim($parts['host'], '[]'); + $port = $parts['port']; + + if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) { + return Promise\reject(new InvalidArgumentException('Invalid target specified')); + } + + // construct URI to SOCKS server to connect to + $socksUri = $this->socksUri; + + // append path from URI if given + if (isset($parts['path'])) { + $socksUri .= $parts['path']; + } + + // parse query args + $args = array(); + if (isset($parts['query'])) { + parse_str($parts['query'], $args); + } + + // append hostname from URI to query string unless explicitly given + if (!isset($args['hostname'])) { + $args['hostname'] = $host; + } + + // append query string + $socksUri .= '?' . http_build_query($args, '', '&'); + + // append fragment from URI if given + if (isset($parts['fragment'])) { + $socksUri .= '#' . $parts['fragment']; + } + + // start TCP/IP connection to SOCKS server + $connecting = $this->connector->connect($socksUri); + + $deferred = new Deferred(function ($_, $reject) use ($uri, $connecting) { + $reject(new RuntimeException( + 'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + )); + + // either close active connection or cancel pending connection attempt + $connecting->then(function (ConnectionInterface $stream) { + $stream->close(); + }); + $connecting->cancel(); + }); + + // handle SOCKS protocol once connection is ready + // resolve plain connection once SOCKS protocol is completed + $that = $this; + $connecting->then( + function (ConnectionInterface $stream) use ($that, $host, $port, $deferred, $uri) { + $that->handleConnectedSocks($stream, $host, $port, $deferred, $uri); + }, + function (Exception $e) use ($uri, $deferred) { + $deferred->reject($e = new RuntimeException( + 'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, + $e + )); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $r->setAccessible(true); + $trace = $r->getValue($e); + + // Exception trace arguments are not available on some PHP 7.4 installs + // @codeCoverageIgnoreStart + foreach ($trace as $ti => $one) { + if (isset($one['args'])) { + foreach ($one['args'] as $ai => $arg) { + if ($arg instanceof \Closure) { + $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($e, $trace); + } + ); + + return $deferred->promise(); + } + + /** + * Internal helper used to handle the communication with the SOCKS server + * + * @param ConnectionInterface $stream + * @param string $host + * @param int $port + * @param Deferred $deferred + * @param string $uri + * @return void + * @internal + */ + public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred, $uri) + { + $reader = new StreamReader(); + $stream->on('data', array($reader, 'write')); + + $stream->on('error', $onError = function (Exception $e) use ($deferred, $uri) { + $deferred->reject(new RuntimeException( + 'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)', + defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e) + ); + }); + + $stream->on('close', $onClose = function () use ($deferred, $uri) { + $deferred->reject(new RuntimeException( + 'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response from proxy (ECONNRESET)', + defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104) + ); + }); + + if ($this->protocolVersion === 5) { + $promise = $this->handleSocks5($stream, $host, $port, $reader, $uri); + } else { + $promise = $this->handleSocks4($stream, $host, $port, $reader, $uri); + } + + $promise->then(function () use ($deferred, $stream, $reader, $onError, $onClose) { + $stream->removeListener('data', array($reader, 'write')); + $stream->removeListener('error', $onError); + $stream->removeListener('close', $onClose); + + $deferred->resolve($stream); + }, function (Exception $error) use ($deferred, $stream, $uri) { + // pass custom RuntimeException through as-is, otherwise wrap in protocol error + if (!$error instanceof RuntimeException) { + $error = new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)', + defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, + $error + ); + } + + $deferred->reject($error); + $stream->close(); + }); + } + + private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri) + { + // do not resolve hostname. only try to convert to IP + $ip = ip2long($host); + + // send IP or (0.0.0.1) if invalid + $data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00); + + if ($ip === false) { + // host is not a valid IP => send along hostname (SOCKS4a) + $data .= $host . pack('C', 0x00); + } + + $stream->write($data); + + return $reader->readBinary(array( + 'null' => 'C', + 'status' => 'C', + 'port' => 'n', + 'ip' => 'N' + ))->then(function ($data) use ($uri) { + if ($data['null'] !== 0x00) { + throw new Exception('Invalid SOCKS response'); + } + if ($data['status'] !== 0x5a) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy refused connection with error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } + }); + } + + private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri) + { + // protocol version 5 + $data = pack('C', 0x05); + + $auth = $this->auth; + if ($auth === null) { + // one method, no authentication + $data .= pack('C2', 0x01, 0x00); + } else { + // two methods, username/password and no authentication + $data .= pack('C3', 0x02, 0x02, 0x00); + } + $stream->write($data); + + $that = $this; + + return $reader->readBinary(array( + 'version' => 'C', + 'method' => 'C' + ))->then(function ($data) use ($auth, $stream, $reader, $uri) { + if ($data['version'] !== 0x05) { + throw new Exception('Version/Protocol mismatch'); + } + + if ($data['method'] === 0x02 && $auth !== null) { + // username/password authentication requested and provided + $stream->write($auth); + + return $reader->readBinary(array( + 'version' => 'C', + 'status' => 'C' + ))->then(function ($data) use ($uri) { + if ($data['version'] !== 0x01 || $data['status'] !== 0x00) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy denied access with given authentication details (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13 + ); + } + }); + } else if ($data['method'] !== 0x00) { + // any other method than "no authentication" + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy denied access due to unsupported authentication method (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13 + ); + } + })->then(function () use ($stream, $reader, $host, $port) { + // do not resolve hostname. only try to convert to (binary/packed) IP + $ip = @inet_pton($host); + + $data = pack('C3', 0x05, 0x01, 0x00); + if ($ip === false) { + // not an IP, send as hostname + $data .= pack('C2', 0x03, strlen($host)) . $host; + } else { + // send as IPv4 / IPv6 + $data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip; + } + $data .= pack('n', $port); + + $stream->write($data); + + return $reader->readBinary(array( + 'version' => 'C', + 'status' => 'C', + 'null' => 'C', + 'type' => 'C' + )); + })->then(function ($data) use ($reader, $uri) { + if ($data['version'] !== 0x05 || $data['null'] !== 0x00) { + throw new Exception('Invalid SOCKS response'); + } + if ($data['status'] !== 0x00) { + // map limited list of SOCKS error codes to common socket error conditions + // @link https://tools.ietf.org/html/rfc1928#section-6 + if ($data['status'] === Server::ERROR_GENERAL) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy refused connection with general server failure (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy denied access due to ruleset (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13 + ); + } elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported network unreachable (ENETUNREACH)', + defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101 + ); + } elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported host unreachable (EHOSTUNREACH)', + defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113 + ); + } elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } elseif ($data['status'] === Server::ERROR_TTL) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported TTL/timeout expired (ETIMEDOUT)', + defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110 + ); + } elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy does not support the CONNECT command (EPROTO)', + defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71 + ); + } elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy does not support this address type (EPROTO)', + defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71 + ); + } + + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy server refused connection with unknown error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } + if ($data['type'] === 0x01) { + // IPv4 address => skip IP and port + return $reader->readLength(6); + } elseif ($data['type'] === 0x03) { + // domain name => read domain name length + return $reader->readBinary(array( + 'length' => 'C' + ))->then(function ($data) use ($reader) { + // skip domain name and port + return $reader->readLength($data['length'] + 2); + }); + } elseif ($data['type'] === 0x04) { + // IPv6 address => skip IP and port + return $reader->readLength(18); + } else { + throw new Exception('Invalid SOCKS reponse: Invalid address type'); + } + }); + } +} diff --git a/vendor/clue/socks-react/src/Server.php b/vendor/clue/socks-react/src/Server.php new file mode 100644 index 0000000..ca45015 --- /dev/null +++ b/vendor/clue/socks-react/src/Server.php @@ -0,0 +1,416 @@ +<?php + +namespace Clue\React\Socks; + +use React\Socket\ServerInterface; +use React\Promise\PromiseInterface; +use React\Socket\ConnectorInterface; +use React\Socket\Connector; +use React\Socket\ConnectionInterface; +use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; +use \UnexpectedValueException; +use \InvalidArgumentException; +use \Exception; +use React\Promise\Timer\TimeoutException; + +final class Server +{ + // the following error codes are only used for SOCKS5 only + /** @internal */ + const ERROR_GENERAL = 0x01; + /** @internal */ + const ERROR_NOT_ALLOWED_BY_RULESET = 0x02; + /** @internal */ + const ERROR_NETWORK_UNREACHABLE = 0x03; + /** @internal */ + const ERROR_HOST_UNREACHABLE = 0x04; + /** @internal */ + const ERROR_CONNECTION_REFUSED = 0x05; + /** @internal */ + const ERROR_TTL = 0x06; + /** @internal */ + const ERROR_COMMAND_UNSUPPORTED = 0x07; + /** @internal */ + const ERROR_ADDRESS_UNSUPPORTED = 0x08; + + /** @var LoopInterface */ + private $loop; + + /** @var ConnectorInterface */ + private $connector; + + /** + * @var null|callable + */ + private $auth; + + /** + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param ?LoopInterface $loop + * @param ?ConnectorInterface $connector + * @param null|array|callable $auth + */ + public function __construct( + LoopInterface $loop = null, + ConnectorInterface $connector = null, + #[\SensitiveParameter] + $auth = null + ) { + if (\is_array($auth)) { + // wrap authentication array in authentication callback + $this->auth = function ( + $username, + #[\SensitiveParameter] + $password + ) use ($auth) { + return \React\Promise\resolve( + isset($auth[$username]) && (string)$auth[$username] === $password + ); + }; + } elseif (\is_callable($auth)) { + // wrap authentication callback in order to cast its return value to a promise + $this->auth = function( + $username, + #[\SensitiveParameter] + $password, + #[\SensitiveParameter] + $remote + ) use ($auth) { + return \React\Promise\resolve( + \call_user_func($auth, $username, $password, $remote) + ); + }; + } elseif ($auth !== null) { + throw new \InvalidArgumentException('Invalid authenticator given'); + } + + $this->loop = $loop ?: Loop::get(); + $this->connector = $connector ?: new Connector(array(), $this->loop); + } + + /** + * @param ServerInterface $socket + * @return void + */ + public function listen(ServerInterface $socket) + { + $that = $this; + $socket->on('connection', function ($connection) use ($that) { + $that->onConnection($connection); + }); + } + + /** @internal */ + public function onConnection(ConnectionInterface $connection) + { + $that = $this; + $handling = $this->handleSocks($connection)->then(null, function () use ($connection, $that) { + // SOCKS failed => close connection + $that->endConnection($connection); + }); + + $connection->on('close', function () use ($handling) { + $handling->cancel(); + }); + } + + /** + * [internal] gracefully shutdown connection by flushing all remaining data and closing stream + * + * @internal + */ + public function endConnection(ConnectionInterface $stream) + { + $tid = true; + $loop = $this->loop; + + // cancel below timer in case connection is closed in time + $stream->once('close', function () use (&$tid, $loop) { + // close event called before the timer was set up, so everything is okay + if ($tid === true) { + // make sure to not start a useless timer + $tid = false; + } else { + $loop->cancelTimer($tid); + } + }); + + // shut down connection by pausing input data, flushing outgoing buffer and then exit + $stream->pause(); + $stream->end(); + + // check if connection is not already closed + if ($tid === true) { + // fall back to forcefully close connection in 3 seconds if buffer can not be flushed + $tid = $loop->addTimer(3.0, array($stream,'close')); + } + } + + private function handleSocks(ConnectionInterface $stream) + { + $reader = new StreamReader(); + $stream->on('data', array($reader, 'write')); + + $that = $this; + $auth = $this->auth; + + return $reader->readByte()->then(function ($version) use ($stream, $that, $auth, $reader){ + if ($version === 0x04) { + if ($auth !== null) { + throw new UnexpectedValueException('SOCKS4 not allowed because authentication is required'); + } + return $that->handleSocks4($stream, $reader); + } else if ($version === 0x05) { + return $that->handleSocks5($stream, $auth, $reader); + } + throw new UnexpectedValueException('Unexpected/unknown version number'); + }); + } + + /** @internal */ + public function handleSocks4(ConnectionInterface $stream, StreamReader $reader) + { + $remote = $stream->getRemoteAddress(); + if ($remote !== null) { + // remove transport scheme and prefix socks4:// instead + $secure = strpos($remote, 'tls://') === 0; + if (($pos = strpos($remote, '://')) !== false) { + $remote = substr($remote, $pos + 3); + } + $remote = 'socks4' . ($secure ? 's' : '') . '://' . $remote; + } + + $that = $this; + return $reader->readByteAssert(0x01)->then(function () use ($reader) { + return $reader->readBinary(array( + 'port' => 'n', + 'ipLong' => 'N', + 'null' => 'C' + )); + })->then(function ($data) use ($reader, $remote) { + if ($data['null'] !== 0x00) { + throw new Exception('Not a null byte'); + } + if ($data['ipLong'] === 0) { + throw new Exception('Invalid IP'); + } + if ($data['port'] === 0) { + throw new Exception('Invalid port'); + } + if ($data['ipLong'] < 256) { + // invalid IP => probably a SOCKS4a request which appends the hostname + return $reader->readStringNull()->then(function ($string) use ($data, $remote){ + return array($string, $data['port'], $remote); + }); + } else { + $ip = long2ip($data['ipLong']); + return array($ip, $data['port'], $remote); + } + })->then(function ($target) use ($stream, $that) { + return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){ + $stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); + + return $remote; + }, function($error) use ($stream){ + $stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); + + throw $error; + }); + }, function($error) { + throw new UnexpectedValueException('SOCKS4 protocol error',0,$error); + }); + } + + /** @internal */ + public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader) + { + $remote = $stream->getRemoteAddress(); + if ($remote !== null) { + // remove transport scheme and prefix socks5:// instead + $secure = strpos($remote, 'tls://') === 0; + if (($pos = strpos($remote, '://')) !== false) { + $remote = substr($remote, $pos + 3); + } + $remote = 'socks' . ($secure ? 's' : '') . '://' . $remote; + } + + $that = $this; + return $reader->readByte()->then(function ($num) use ($reader) { + // $num different authentication mechanisms offered + return $reader->readLength($num); + })->then(function ($methods) use ($reader, $stream, $auth, &$remote) { + if ($auth === null && strpos($methods,"\x00") !== false) { + // accept "no authentication" + $stream->write(pack('C2', 0x05, 0x00)); + + return 0x00; + } else if ($auth !== null && strpos($methods,"\x02") !== false) { + // username/password authentication (RFC 1929) sub negotiation + $stream->write(pack('C2', 0x05, 0x02)); + return $reader->readByteAssert(0x01)->then(function () use ($reader) { + return $reader->readByte(); + })->then(function ($length) use ($reader) { + return $reader->readLength($length); + })->then(function ($username) use ($reader, $auth, $stream, &$remote) { + return $reader->readByte()->then(function ($length) use ($reader) { + return $reader->readLength($length); + })->then(function ( + #[\SensitiveParameter] + $password + ) use ($username, $auth, $stream, &$remote) { + // username and password given => authenticate + + // prefix username/password to remote URI + if ($remote !== null) { + $remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote); + } + + return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) { + if ($authenticated) { + // accept auth + $stream->write(pack('C2', 0x01, 0x00)); + } else { + // reject auth => send any code but 0x00 + $stream->end(pack('C2', 0x01, 0xFF)); + throw new UnexpectedValueException('Authentication denied'); + } + }, function ($e) use ($stream) { + // reject failed authentication => send any code but 0x00 + $stream->end(pack('C2', 0x01, 0xFF)); + throw new UnexpectedValueException('Authentication error', 0, $e); + }); + }); + }); + } else { + // reject all offered authentication methods + $stream->write(pack('C2', 0x05, 0xFF)); + throw new UnexpectedValueException('No acceptable authentication mechanism found'); + } + })->then(function ($method) use ($reader) { + return $reader->readBinary(array( + 'version' => 'C', + 'command' => 'C', + 'null' => 'C', + 'type' => 'C' + )); + })->then(function ($data) use ($reader) { + if ($data['version'] !== 0x05) { + throw new UnexpectedValueException('Invalid SOCKS version'); + } + if ($data['command'] !== 0x01) { + throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED); + } +// if ($data['null'] !== 0x00) { +// throw new UnexpectedValueException('Reserved byte has to be NULL'); +// } + if ($data['type'] === 0x03) { + // target hostname string + return $reader->readByte()->then(function ($len) use ($reader) { + return $reader->readLength($len); + }); + } else if ($data['type'] === 0x01) { + // target IPv4 + return $reader->readLength(4)->then(function ($addr) { + return inet_ntop($addr); + }); + } else if ($data['type'] === 0x04) { + // target IPv6 + return $reader->readLength(16)->then(function ($addr) { + return inet_ntop($addr); + }); + } else { + throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED); + } + })->then(function ($host) use ($reader, &$remote) { + return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) { + return array($host, $data['port'], $remote); + }); + })->then(function ($target) use ($that, $stream) { + return $that->connectTarget($stream, $target); + }, function($error) use ($stream) { + throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error); + })->then(function (ConnectionInterface $remote) use ($stream) { + $stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0)); + + return $remote; + }, function(Exception $error) use ($stream){ + $stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0)); + + throw $error; + }); + } + + /** @internal */ + public function connectTarget(ConnectionInterface $stream, array $target) + { + $uri = $target[0]; + if (strpos($uri, ':') !== false) { + $uri = '[' . $uri . ']'; + } + $uri .= ':' . $target[1]; + + // validate URI so a string hostname can not pass excessive URI parts + $parts = parse_url('tcp://' . $uri); + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) { + return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given')); + } + + if (isset($target[2])) { + $uri .= '?source=' . rawurlencode($target[2]); + } + + $that = $this; + $connecting = $this->connector->connect($uri); + + $stream->on('close', function () use ($connecting) { + $connecting->cancel(); + }); + + return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) { + $stream->pipe($remote, array('end'=>false)); + $remote->pipe($stream, array('end'=>false)); + + // remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local + $remote->on('end', function() use ($stream, $that) { + $that->endConnection($stream); + }); + + // local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote + $stream->on('end', function() use ($remote, $that) { + $that->endConnection($remote); + }); + + // set bigger buffer size of 100k to improve performance + $stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024; + + return $remote; + }, function(Exception $error) { + // default to general/unknown error + $code = Server::ERROR_GENERAL; + + // map common socket error conditions to limited list of SOCKS error codes + if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) { + $code = Server::ERROR_NOT_ALLOWED_BY_RULESET; + } elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) { + $code = Server::ERROR_HOST_UNREACHABLE; + } elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) { + $code = Server::ERROR_NETWORK_UNREACHABLE; + } elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') { + // Socket component does not currently assign an error code for this, so we have to resort to checking the exception message + $code = Server::ERROR_CONNECTION_REFUSED; + } elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) { + // Socket component does not currently assign an error code for this, but we can rely on the TimeoutException + $code = Server::ERROR_TTL; + } + + throw new UnexpectedValueException('Unable to connect to remote target', $code, $error); + }); + } +} diff --git a/vendor/clue/socks-react/src/StreamReader.php b/vendor/clue/socks-react/src/StreamReader.php new file mode 100644 index 0000000..f01d252 --- /dev/null +++ b/vendor/clue/socks-react/src/StreamReader.php @@ -0,0 +1,149 @@ +<?php + +namespace Clue\React\Socks; + +use React\Promise\Deferred; +use \InvalidArgumentException; +use \UnexpectedValueException; + +/** + * @internal + */ +final class StreamReader +{ + const RET_DONE = true; + const RET_INCOMPLETE = null; + + private $buffer = ''; + private $queue = array(); + + public function write($data) + { + $this->buffer .= $data; + + do { + $current = reset($this->queue); + + if ($current === false) { + break; + } + + /* @var $current Closure */ + + $ret = $current($this->buffer); + + if ($ret === self::RET_INCOMPLETE) { + // current is incomplete, so wait for further data to arrive + break; + } else { + // current is done, remove from list and continue with next + array_shift($this->queue); + } + } while (true); + } + + public function readBinary($structure) + { + $length = 0; + $unpack = ''; + foreach ($structure as $name=>$format) { + if ($length !== 0) { + $unpack .= '/'; + } + $unpack .= $format . $name; + + if ($format === 'C') { + ++$length; + } else if ($format === 'n') { + $length += 2; + } else if ($format === 'N') { + $length += 4; + } else { + throw new InvalidArgumentException('Invalid format given'); + } + } + + return $this->readLength($length)->then(function ($response) use ($unpack) { + return unpack($unpack, $response); + }); + } + + public function readLength($bytes) + { + $deferred = new Deferred(); + + $this->readBufferCallback(function (&$buffer) use ($bytes, $deferred) { + if (strlen($buffer) >= $bytes) { + $deferred->resolve((string)substr($buffer, 0, $bytes)); + $buffer = (string)substr($buffer, $bytes); + + return StreamReader::RET_DONE; + } + }); + + return $deferred->promise(); + } + + public function readByte() + { + return $this->readBinary(array( + 'byte' => 'C' + ))->then(function ($data) { + return $data['byte']; + }); + } + + public function readByteAssert($expect) + { + return $this->readByte()->then(function ($byte) use ($expect) { + if ($byte !== $expect) { + throw new UnexpectedValueException('Unexpected byte encountered'); + } + return $byte; + }); + } + + public function readStringNull() + { + $deferred = new Deferred(); + $string = ''; + + $that = $this; + $readOne = function () use (&$readOne, $that, $deferred, &$string) { + $that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) { + if ($byte === 0x00) { + $deferred->resolve($string); + } else { + $string .= chr($byte); + $readOne(); + } + }); + }; + $readOne(); + + return $deferred->promise(); + } + + public function readBufferCallback(/* callable */ $callable) + { + if (!is_callable($callable)) { + throw new InvalidArgumentException('Given function must be callable'); + } + + if ($this->queue) { + $this->queue []= $callable; + } else { + $this->queue = array($callable); + + if ($this->buffer !== '') { + // this is the first element in the queue and the buffer is filled => trigger write procedure + $this->write(''); + } + } + } + + public function getBuffer() + { + return $this->buffer; + } +} |