diff options
Diffstat (limited to 'vendor/clue/socks-react')
-rw-r--r-- | vendor/clue/socks-react/LICENSE | 21 | ||||
-rw-r--r-- | vendor/clue/socks-react/composer.json | 31 | ||||
-rw-r--r-- | vendor/clue/socks-react/src/Client.php | 452 | ||||
-rw-r--r-- | vendor/clue/socks-react/src/Server.php | 399 | ||||
-rw-r--r-- | vendor/clue/socks-react/src/StreamReader.php | 149 |
5 files changed, 1052 insertions, 0 deletions
diff --git a/vendor/clue/socks-react/LICENSE b/vendor/clue/socks-react/LICENSE new file mode 100644 index 0000000..8efa9aa --- /dev/null +++ b/vendor/clue/socks-react/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2011 Christian Lück + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/clue/socks-react/composer.json b/vendor/clue/socks-react/composer.json new file mode 100644 index 0000000..c57742b --- /dev/null +++ b/vendor/clue/socks-react/composer.json @@ -0,0 +1,31 @@ +{ + "name": "clue/socks-react", + "description": "Async SOCKS proxy connector client and server implementation, tunnel any TCP/IP-based protocol through a SOCKS5 or SOCKS4(a) proxy server, built on top of ReactPHP.", + "keywords": ["socks client", "socks server", "socks5", "socks4a", "proxy server", "tcp tunnel", "async", "ReactPHP"], + "homepage": "https://github.com/clue/reactphp-socks", + "license": "MIT", + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "require": { + "php": ">=5.3", + "react/promise": "^2.1 || ^1.2", + "react/socket": "^1.9" + }, + "require-dev": { + "clue/block-react": "^1.1", + "clue/connection-manager-extra": "^1.0 || ^0.7", + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2", + "react/http": "^1.5" + }, + "autoload": { + "psr-4": { "Clue\\React\\Socks\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Clue\\Tests\\React\\Socks\\": "tests/" } + } +} diff --git a/vendor/clue/socks-react/src/Client.php b/vendor/clue/socks-react/src/Client.php new file mode 100644 index 0000000..5350b54 --- /dev/null +++ b/vendor/clue/socks-react/src/Client.php @@ -0,0 +1,452 @@ +<?php + +namespace Clue\React\Socks; + +use React\Promise; +use React\Promise\PromiseInterface; +use React\Promise\Deferred; +use React\Socket\ConnectionInterface; +use React\Socket\Connector; +use React\Socket\ConnectorInterface; +use React\Socket\FixedUriConnector; +use Exception; +use InvalidArgumentException; +use RuntimeException; + +final class Client implements ConnectorInterface +{ + /** + * @var ConnectorInterface + */ + private $connector; + + private $socksUri; + + private $protocolVersion = 5; + + private $auth = null; + + /** + * @param string $socksUri + * @param ?ConnectorInterface $connector + * @throws InvalidArgumentException + */ + public function __construct($socksUri, ConnectorInterface $connector = null) + { + // support `sockss://` scheme for SOCKS over TLS + // support `socks+unix://` scheme for Unix domain socket (UDS) paths + if (preg_match('/^(socks(?:5|4)?)(s|\+unix):\/\/(.*?@)?(.+?)$/', $socksUri, $match)) { + // rewrite URI to parse SOCKS scheme, authentication and dummy host + $socksUri = $match[1] . '://' . $match[3] . 'localhost'; + + // connector uses appropriate transport scheme and explicit host given + $connector = new FixedUriConnector( + ($match[2] === 's' ? 'tls://' : 'unix://') . $match[4], + $connector ?: new Connector() + ); + } + + // assume default scheme if none is given + if (strpos($socksUri, '://') === false) { + $socksUri = 'socks://' . $socksUri; + } + + // parse URI into individual parts + $parts = parse_url($socksUri); + if (!$parts || !isset($parts['scheme'], $parts['host'])) { + throw new InvalidArgumentException('Invalid SOCKS server URI "' . $socksUri . '"'); + } + + // assume default port + if (!isset($parts['port'])) { + $parts['port'] = 1080; + } + + // user or password in URI => SOCKS5 authentication + if (isset($parts['user']) || isset($parts['pass'])) { + if ($parts['scheme'] !== 'socks' && $parts['scheme'] !== 'socks5') { + // fail if any other protocol version given explicitly + throw new InvalidArgumentException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication'); + } + $parts += array('user' => '', 'pass' => ''); + $this->setAuth(rawurldecode($parts['user']), rawurldecode($parts['pass'])); + } + + // check for valid protocol version from URI scheme + $this->setProtocolVersionFromScheme($parts['scheme']); + + $this->socksUri = $parts['host'] . ':' . $parts['port']; + $this->connector = $connector ?: new Connector(); + } + + private function setProtocolVersionFromScheme($scheme) + { + if ($scheme === 'socks' || $scheme === 'socks5') { + $this->protocolVersion = 5; + } elseif ($scheme === 'socks4') { + $this->protocolVersion = 4; + } else { + throw new InvalidArgumentException('Invalid protocol version given "' . $scheme . '://"'); + } + } + + /** + * set login data for username/password authentication method (RFC1929) + * + * @param string $username + * @param string $password + * @link http://tools.ietf.org/html/rfc1929 + */ + private function setAuth($username, $password) + { + if (strlen($username) > 255 || strlen($password) > 255) { + throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each'); + } + $this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password; + } + + /** + * Establish a TCP/IP connection to the given target URI through the SOCKS server + * + * Many higher-level networking protocols build on top of TCP. It you're dealing + * with one such client implementation, it probably uses/accepts an instance + * implementing ReactPHP's `ConnectorInterface` (and usually its default `Connector` + * instance). In this case you can also pass this `Connector` instance instead + * to make this client implementation SOCKS-aware. That's it. + * + * @param string $uri + * @return PromiseInterface Promise<ConnectionInterface,Exception> + */ + public function connect($uri) + { + if (strpos($uri, '://') === false) { + $uri = 'tcp://' . $uri; + } + + $parts = parse_url($uri); + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') { + return Promise\reject(new InvalidArgumentException('Invalid target URI specified')); + } + + $host = trim($parts['host'], '[]'); + $port = $parts['port']; + + if (strlen($host) > 255 || $port > 65535 || $port < 0 || (string)$port !== (string)(int)$port) { + return Promise\reject(new InvalidArgumentException('Invalid target specified')); + } + + // construct URI to SOCKS server to connect to + $socksUri = $this->socksUri; + + // append path from URI if given + if (isset($parts['path'])) { + $socksUri .= $parts['path']; + } + + // parse query args + $args = array(); + if (isset($parts['query'])) { + parse_str($parts['query'], $args); + } + + // append hostname from URI to query string unless explicitly given + if (!isset($args['hostname'])) { + $args['hostname'] = $host; + } + + // append query string + $socksUri .= '?' . http_build_query($args, '', '&'); + + // append fragment from URI if given + if (isset($parts['fragment'])) { + $socksUri .= '#' . $parts['fragment']; + } + + // start TCP/IP connection to SOCKS server + $connecting = $this->connector->connect($socksUri); + + $deferred = new Deferred(function ($_, $reject) use ($uri, $connecting) { + $reject(new RuntimeException( + 'Connection to ' . $uri . ' cancelled while waiting for proxy (ECONNABORTED)', + defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103 + )); + + // either close active connection or cancel pending connection attempt + $connecting->then(function (ConnectionInterface $stream) { + $stream->close(); + }); + $connecting->cancel(); + }); + + // handle SOCKS protocol once connection is ready + // resolve plain connection once SOCKS protocol is completed + $that = $this; + $connecting->then( + function (ConnectionInterface $stream) use ($that, $host, $port, $deferred, $uri) { + $that->handleConnectedSocks($stream, $host, $port, $deferred, $uri); + }, + function (Exception $e) use ($uri, $deferred) { + $deferred->reject($e = new RuntimeException( + 'Connection to ' . $uri . ' failed because connection to proxy failed (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, + $e + )); + + // avoid garbage references by replacing all closures in call stack. + // what a lovely piece of code! + $r = new \ReflectionProperty('Exception', 'trace'); + $r->setAccessible(true); + $trace = $r->getValue($e); + + // Exception trace arguments are not available on some PHP 7.4 installs + // @codeCoverageIgnoreStart + foreach ($trace as &$one) { + if (isset($one['args'])) { + foreach ($one['args'] as &$arg) { + if ($arg instanceof \Closure) { + $arg = 'Object(' . \get_class($arg) . ')'; + } + } + } + } + // @codeCoverageIgnoreEnd + $r->setValue($e, $trace); + } + ); + + return $deferred->promise(); + } + + /** + * Internal helper used to handle the communication with the SOCKS server + * + * @param ConnectionInterface $stream + * @param string $host + * @param int $port + * @param Deferred $deferred + * @param string $uri + * @return void + * @internal + */ + public function handleConnectedSocks(ConnectionInterface $stream, $host, $port, Deferred $deferred, $uri) + { + $reader = new StreamReader(); + $stream->on('data', array($reader, 'write')); + + $stream->on('error', $onError = function (Exception $e) use ($deferred, $uri) { + $deferred->reject(new RuntimeException( + 'Connection to ' . $uri . ' failed because connection to proxy caused a stream error (EIO)', + defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e) + ); + }); + + $stream->on('close', $onClose = function () use ($deferred, $uri) { + $deferred->reject(new RuntimeException( + 'Connection to ' . $uri . ' failed because connection to proxy was lost while waiting for response from proxy (ECONNRESET)', + defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104) + ); + }); + + if ($this->protocolVersion === 5) { + $promise = $this->handleSocks5($stream, $host, $port, $reader, $uri); + } else { + $promise = $this->handleSocks4($stream, $host, $port, $reader, $uri); + } + + $promise->then(function () use ($deferred, $stream, $reader, $onError, $onClose) { + $stream->removeListener('data', array($reader, 'write')); + $stream->removeListener('error', $onError); + $stream->removeListener('close', $onClose); + + $deferred->resolve($stream); + }, function (Exception $error) use ($deferred, $stream, $uri) { + // pass custom RuntimeException through as-is, otherwise wrap in protocol error + if (!$error instanceof RuntimeException) { + $error = new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy returned invalid response (EBADMSG)', + defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, + $error + ); + } + + $deferred->reject($error); + $stream->close(); + }); + } + + private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri) + { + // do not resolve hostname. only try to convert to IP + $ip = ip2long($host); + + // send IP or (0.0.0.1) if invalid + $data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00); + + if ($ip === false) { + // host is not a valid IP => send along hostname (SOCKS4a) + $data .= $host . pack('C', 0x00); + } + + $stream->write($data); + + return $reader->readBinary(array( + 'null' => 'C', + 'status' => 'C', + 'port' => 'n', + 'ip' => 'N' + ))->then(function ($data) use ($uri) { + if ($data['null'] !== 0x00) { + throw new Exception('Invalid SOCKS response'); + } + if ($data['status'] !== 0x5a) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy refused connection with error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } + }); + } + + private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamReader $reader, $uri) + { + // protocol version 5 + $data = pack('C', 0x05); + + $auth = $this->auth; + if ($auth === null) { + // one method, no authentication + $data .= pack('C2', 0x01, 0x00); + } else { + // two methods, username/password and no authentication + $data .= pack('C3', 0x02, 0x02, 0x00); + } + $stream->write($data); + + $that = $this; + + return $reader->readBinary(array( + 'version' => 'C', + 'method' => 'C' + ))->then(function ($data) use ($auth, $stream, $reader, $uri) { + if ($data['version'] !== 0x05) { + throw new Exception('Version/Protocol mismatch'); + } + + if ($data['method'] === 0x02 && $auth !== null) { + // username/password authentication requested and provided + $stream->write($auth); + + return $reader->readBinary(array( + 'version' => 'C', + 'status' => 'C' + ))->then(function ($data) use ($uri) { + if ($data['version'] !== 0x01 || $data['status'] !== 0x00) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy denied access with given authentication details (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13 + ); + } + }); + } else if ($data['method'] !== 0x00) { + // any other method than "no authentication" + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy denied access due to unsupported authentication method (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13 + ); + } + })->then(function () use ($stream, $reader, $host, $port) { + // do not resolve hostname. only try to convert to (binary/packed) IP + $ip = @inet_pton($host); + + $data = pack('C3', 0x05, 0x01, 0x00); + if ($ip === false) { + // not an IP, send as hostname + $data .= pack('C2', 0x03, strlen($host)) . $host; + } else { + // send as IPv4 / IPv6 + $data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip; + } + $data .= pack('n', $port); + + $stream->write($data); + + return $reader->readBinary(array( + 'version' => 'C', + 'status' => 'C', + 'null' => 'C', + 'type' => 'C' + )); + })->then(function ($data) use ($reader, $uri) { + if ($data['version'] !== 0x05 || $data['null'] !== 0x00) { + throw new Exception('Invalid SOCKS response'); + } + if ($data['status'] !== 0x00) { + // map limited list of SOCKS error codes to common socket error conditions + // @link https://tools.ietf.org/html/rfc1928#section-6 + if ($data['status'] === Server::ERROR_GENERAL) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy refused connection with general server failure (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } elseif ($data['status'] === Server::ERROR_NOT_ALLOWED_BY_RULESET) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy denied access due to ruleset (EACCES)', + defined('SOCKET_EACCES') ? SOCKET_EACCES : 13 + ); + } elseif ($data['status'] === Server::ERROR_NETWORK_UNREACHABLE) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported network unreachable (ENETUNREACH)', + defined('SOCKET_ENETUNREACH') ? SOCKET_ENETUNREACH : 101 + ); + } elseif ($data['status'] === Server::ERROR_HOST_UNREACHABLE) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported host unreachable (EHOSTUNREACH)', + defined('SOCKET_EHOSTUNREACH') ? SOCKET_EHOSTUNREACH : 113 + ); + } elseif ($data['status'] === Server::ERROR_CONNECTION_REFUSED) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported connection refused (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } elseif ($data['status'] === Server::ERROR_TTL) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy reported TTL/timeout expired (ETIMEDOUT)', + defined('SOCKET_ETIMEDOUT') ? SOCKET_ETIMEDOUT : 110 + ); + } elseif ($data['status'] === Server::ERROR_COMMAND_UNSUPPORTED) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy does not support the CONNECT command (EPROTO)', + defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71 + ); + } elseif ($data['status'] === Server::ERROR_ADDRESS_UNSUPPORTED) { + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy does not support this address type (EPROTO)', + defined('SOCKET_EPROTO') ? SOCKET_EPROTO : 71 + ); + } + + throw new RuntimeException( + 'Connection to ' . $uri . ' failed because proxy server refused connection with unknown error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', + defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111 + ); + } + if ($data['type'] === 0x01) { + // IPv4 address => skip IP and port + return $reader->readLength(6); + } elseif ($data['type'] === 0x03) { + // domain name => read domain name length + return $reader->readBinary(array( + 'length' => 'C' + ))->then(function ($data) use ($reader) { + // skip domain name and port + return $reader->readLength($data['length'] + 2); + }); + } elseif ($data['type'] === 0x04) { + // IPv6 address => skip IP and port + return $reader->readLength(18); + } else { + throw new Exception('Invalid SOCKS reponse: Invalid address type'); + } + }); + } +} diff --git a/vendor/clue/socks-react/src/Server.php b/vendor/clue/socks-react/src/Server.php new file mode 100644 index 0000000..2405f3e --- /dev/null +++ b/vendor/clue/socks-react/src/Server.php @@ -0,0 +1,399 @@ +<?php + +namespace Clue\React\Socks; + +use React\Socket\ServerInterface; +use React\Promise\PromiseInterface; +use React\Socket\ConnectorInterface; +use React\Socket\Connector; +use React\Socket\ConnectionInterface; +use React\EventLoop\Loop; +use React\EventLoop\LoopInterface; +use \UnexpectedValueException; +use \InvalidArgumentException; +use \Exception; +use React\Promise\Timer\TimeoutException; + +final class Server +{ + // the following error codes are only used for SOCKS5 only + /** @internal */ + const ERROR_GENERAL = 0x01; + /** @internal */ + const ERROR_NOT_ALLOWED_BY_RULESET = 0x02; + /** @internal */ + const ERROR_NETWORK_UNREACHABLE = 0x03; + /** @internal */ + const ERROR_HOST_UNREACHABLE = 0x04; + /** @internal */ + const ERROR_CONNECTION_REFUSED = 0x05; + /** @internal */ + const ERROR_TTL = 0x06; + /** @internal */ + const ERROR_COMMAND_UNSUPPORTED = 0x07; + /** @internal */ + const ERROR_ADDRESS_UNSUPPORTED = 0x08; + + /** @var LoopInterface */ + private $loop; + + /** @var ConnectorInterface */ + private $connector; + + /** + * @var null|callable + */ + private $auth; + + /** + * + * This class takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use for this object. You can use a `null` value + * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop). + * This value SHOULD NOT be given unless you're sure you want to explicitly use a + * given event loop instance. + * + * @param ?LoopInterface $loop + * @param ?ConnectorInterface $connector + * @param null|array|callable $auth + */ + public function __construct(LoopInterface $loop = null, ConnectorInterface $connector = null, $auth = null) + { + if (\is_array($auth)) { + // wrap authentication array in authentication callback + $this->auth = function ($username, $password) use ($auth) { + return \React\Promise\resolve( + isset($auth[$username]) && (string)$auth[$username] === $password + ); + }; + } elseif (\is_callable($auth)) { + // wrap authentication callback in order to cast its return value to a promise + $this->auth = function($username, $password, $remote) use ($auth) { + return \React\Promise\resolve( + \call_user_func($auth, $username, $password, $remote) + ); + }; + } elseif ($auth !== null) { + throw new \InvalidArgumentException('Invalid authenticator given'); + } + + $this->loop = $loop ?: Loop::get(); + $this->connector = $connector ?: new Connector(array(), $this->loop); + } + + /** + * @param ServerInterface $socket + * @return void + */ + public function listen(ServerInterface $socket) + { + $that = $this; + $socket->on('connection', function ($connection) use ($that) { + $that->onConnection($connection); + }); + } + + /** @internal */ + public function onConnection(ConnectionInterface $connection) + { + $that = $this; + $handling = $this->handleSocks($connection)->then(null, function () use ($connection, $that) { + // SOCKS failed => close connection + $that->endConnection($connection); + }); + + $connection->on('close', function () use ($handling) { + $handling->cancel(); + }); + } + + /** + * [internal] gracefully shutdown connection by flushing all remaining data and closing stream + * + * @internal + */ + public function endConnection(ConnectionInterface $stream) + { + $tid = true; + $loop = $this->loop; + + // cancel below timer in case connection is closed in time + $stream->once('close', function () use (&$tid, $loop) { + // close event called before the timer was set up, so everything is okay + if ($tid === true) { + // make sure to not start a useless timer + $tid = false; + } else { + $loop->cancelTimer($tid); + } + }); + + // shut down connection by pausing input data, flushing outgoing buffer and then exit + $stream->pause(); + $stream->end(); + + // check if connection is not already closed + if ($tid === true) { + // fall back to forcefully close connection in 3 seconds if buffer can not be flushed + $tid = $loop->addTimer(3.0, array($stream,'close')); + } + } + + private function handleSocks(ConnectionInterface $stream) + { + $reader = new StreamReader(); + $stream->on('data', array($reader, 'write')); + + $that = $this; + $auth = $this->auth; + + return $reader->readByte()->then(function ($version) use ($stream, $that, $auth, $reader){ + if ($version === 0x04) { + if ($auth !== null) { + throw new UnexpectedValueException('SOCKS4 not allowed because authentication is required'); + } + return $that->handleSocks4($stream, $reader); + } else if ($version === 0x05) { + return $that->handleSocks5($stream, $auth, $reader); + } + throw new UnexpectedValueException('Unexpected/unknown version number'); + }); + } + + /** @internal */ + public function handleSocks4(ConnectionInterface $stream, StreamReader $reader) + { + $remote = $stream->getRemoteAddress(); + if ($remote !== null) { + // remove transport scheme and prefix socks4:// instead + $secure = strpos($remote, 'tls://') === 0; + if (($pos = strpos($remote, '://')) !== false) { + $remote = substr($remote, $pos + 3); + } + $remote = 'socks4' . ($secure ? 's' : '') . '://' . $remote; + } + + $that = $this; + return $reader->readByteAssert(0x01)->then(function () use ($reader) { + return $reader->readBinary(array( + 'port' => 'n', + 'ipLong' => 'N', + 'null' => 'C' + )); + })->then(function ($data) use ($reader, $remote) { + if ($data['null'] !== 0x00) { + throw new Exception('Not a null byte'); + } + if ($data['ipLong'] === 0) { + throw new Exception('Invalid IP'); + } + if ($data['port'] === 0) { + throw new Exception('Invalid port'); + } + if ($data['ipLong'] < 256) { + // invalid IP => probably a SOCKS4a request which appends the hostname + return $reader->readStringNull()->then(function ($string) use ($data, $remote){ + return array($string, $data['port'], $remote); + }); + } else { + $ip = long2ip($data['ipLong']); + return array($ip, $data['port'], $remote); + } + })->then(function ($target) use ($stream, $that) { + return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){ + $stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); + + return $remote; + }, function($error) use ($stream){ + $stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); + + throw $error; + }); + }, function($error) { + throw new UnexpectedValueException('SOCKS4 protocol error',0,$error); + }); + } + + /** @internal */ + public function handleSocks5(ConnectionInterface $stream, $auth, StreamReader $reader) + { + $remote = $stream->getRemoteAddress(); + if ($remote !== null) { + // remove transport scheme and prefix socks5:// instead + $secure = strpos($remote, 'tls://') === 0; + if (($pos = strpos($remote, '://')) !== false) { + $remote = substr($remote, $pos + 3); + } + $remote = 'socks' . ($secure ? 's' : '') . '://' . $remote; + } + + $that = $this; + return $reader->readByte()->then(function ($num) use ($reader) { + // $num different authentication mechanisms offered + return $reader->readLength($num); + })->then(function ($methods) use ($reader, $stream, $auth, &$remote) { + if ($auth === null && strpos($methods,"\x00") !== false) { + // accept "no authentication" + $stream->write(pack('C2', 0x05, 0x00)); + + return 0x00; + } else if ($auth !== null && strpos($methods,"\x02") !== false) { + // username/password authentication (RFC 1929) sub negotiation + $stream->write(pack('C2', 0x05, 0x02)); + return $reader->readByteAssert(0x01)->then(function () use ($reader) { + return $reader->readByte(); + })->then(function ($length) use ($reader) { + return $reader->readLength($length); + })->then(function ($username) use ($reader, $auth, $stream, &$remote) { + return $reader->readByte()->then(function ($length) use ($reader) { + return $reader->readLength($length); + })->then(function ($password) use ($username, $auth, $stream, &$remote) { + // username and password given => authenticate + + // prefix username/password to remote URI + if ($remote !== null) { + $remote = str_replace('://', '://' . rawurlencode($username) . ':' . rawurlencode($password) . '@', $remote); + } + + return $auth($username, $password, $remote)->then(function ($authenticated) use ($stream) { + if ($authenticated) { + // accept auth + $stream->write(pack('C2', 0x01, 0x00)); + } else { + // reject auth => send any code but 0x00 + $stream->end(pack('C2', 0x01, 0xFF)); + throw new UnexpectedValueException('Authentication denied'); + } + }, function ($e) use ($stream) { + // reject failed authentication => send any code but 0x00 + $stream->end(pack('C2', 0x01, 0xFF)); + throw new UnexpectedValueException('Authentication error', 0, $e); + }); + }); + }); + } else { + // reject all offered authentication methods + $stream->write(pack('C2', 0x05, 0xFF)); + throw new UnexpectedValueException('No acceptable authentication mechanism found'); + } + })->then(function ($method) use ($reader) { + return $reader->readBinary(array( + 'version' => 'C', + 'command' => 'C', + 'null' => 'C', + 'type' => 'C' + )); + })->then(function ($data) use ($reader) { + if ($data['version'] !== 0x05) { + throw new UnexpectedValueException('Invalid SOCKS version'); + } + if ($data['command'] !== 0x01) { + throw new UnexpectedValueException('Only CONNECT requests supported', Server::ERROR_COMMAND_UNSUPPORTED); + } +// if ($data['null'] !== 0x00) { +// throw new UnexpectedValueException('Reserved byte has to be NULL'); +// } + if ($data['type'] === 0x03) { + // target hostname string + return $reader->readByte()->then(function ($len) use ($reader) { + return $reader->readLength($len); + }); + } else if ($data['type'] === 0x01) { + // target IPv4 + return $reader->readLength(4)->then(function ($addr) { + return inet_ntop($addr); + }); + } else if ($data['type'] === 0x04) { + // target IPv6 + return $reader->readLength(16)->then(function ($addr) { + return inet_ntop($addr); + }); + } else { + throw new UnexpectedValueException('Invalid address type', Server::ERROR_ADDRESS_UNSUPPORTED); + } + })->then(function ($host) use ($reader, &$remote) { + return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host, &$remote) { + return array($host, $data['port'], $remote); + }); + })->then(function ($target) use ($that, $stream) { + return $that->connectTarget($stream, $target); + }, function($error) use ($stream) { + throw new UnexpectedValueException('SOCKS5 protocol error', $error->getCode(), $error); + })->then(function (ConnectionInterface $remote) use ($stream) { + $stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0)); + + return $remote; + }, function(Exception $error) use ($stream){ + $stream->write(pack('C4Nn', 0x05, $error->getCode() === 0 ? Server::ERROR_GENERAL : $error->getCode(), 0x00, 0x01, 0, 0)); + + throw $error; + }); + } + + /** @internal */ + public function connectTarget(ConnectionInterface $stream, array $target) + { + $uri = $target[0]; + if (strpos($uri, ':') !== false) { + $uri = '[' . $uri . ']'; + } + $uri .= ':' . $target[1]; + + // validate URI so a string hostname can not pass excessive URI parts + $parts = parse_url('tcp://' . $uri); + if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || count($parts) !== 3) { + return \React\Promise\reject(new InvalidArgumentException('Invalid target URI given')); + } + + if (isset($target[2])) { + $uri .= '?source=' . rawurlencode($target[2]); + } + + $that = $this; + $connecting = $this->connector->connect($uri); + + $stream->on('close', function () use ($connecting) { + $connecting->cancel(); + }); + + return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) { + $stream->pipe($remote, array('end'=>false)); + $remote->pipe($stream, array('end'=>false)); + + // remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local + $remote->on('end', function() use ($stream, $that) { + $that->endConnection($stream); + }); + + // local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote + $stream->on('end', function() use ($remote, $that) { + $that->endConnection($remote); + }); + + // set bigger buffer size of 100k to improve performance + $stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024; + + return $remote; + }, function(Exception $error) { + // default to general/unknown error + $code = Server::ERROR_GENERAL; + + // map common socket error conditions to limited list of SOCKS error codes + if ((defined('SOCKET_EACCES') && $error->getCode() === SOCKET_EACCES) || $error->getCode() === 13) { + $code = Server::ERROR_NOT_ALLOWED_BY_RULESET; + } elseif ((defined('SOCKET_EHOSTUNREACH') && $error->getCode() === SOCKET_EHOSTUNREACH) || $error->getCode() === 113) { + $code = Server::ERROR_HOST_UNREACHABLE; + } elseif ((defined('SOCKET_ENETUNREACH') && $error->getCode() === SOCKET_ENETUNREACH) || $error->getCode() === 101) { + $code = Server::ERROR_NETWORK_UNREACHABLE; + } elseif ((defined('SOCKET_ECONNREFUSED') && $error->getCode() === SOCKET_ECONNREFUSED) || $error->getCode() === 111 || $error->getMessage() === 'Connection refused') { + // Socket component does not currently assign an error code for this, so we have to resort to checking the exception message + $code = Server::ERROR_CONNECTION_REFUSED; + } elseif ((defined('SOCKET_ETIMEDOUT') && $error->getCode() === SOCKET_ETIMEDOUT) || $error->getCode() === 110 || $error instanceof TimeoutException) { + // Socket component does not currently assign an error code for this, but we can rely on the TimeoutException + $code = Server::ERROR_TTL; + } + + throw new UnexpectedValueException('Unable to connect to remote target', $code, $error); + }); + } +} diff --git a/vendor/clue/socks-react/src/StreamReader.php b/vendor/clue/socks-react/src/StreamReader.php new file mode 100644 index 0000000..f01d252 --- /dev/null +++ b/vendor/clue/socks-react/src/StreamReader.php @@ -0,0 +1,149 @@ +<?php + +namespace Clue\React\Socks; + +use React\Promise\Deferred; +use \InvalidArgumentException; +use \UnexpectedValueException; + +/** + * @internal + */ +final class StreamReader +{ + const RET_DONE = true; + const RET_INCOMPLETE = null; + + private $buffer = ''; + private $queue = array(); + + public function write($data) + { + $this->buffer .= $data; + + do { + $current = reset($this->queue); + + if ($current === false) { + break; + } + + /* @var $current Closure */ + + $ret = $current($this->buffer); + + if ($ret === self::RET_INCOMPLETE) { + // current is incomplete, so wait for further data to arrive + break; + } else { + // current is done, remove from list and continue with next + array_shift($this->queue); + } + } while (true); + } + + public function readBinary($structure) + { + $length = 0; + $unpack = ''; + foreach ($structure as $name=>$format) { + if ($length !== 0) { + $unpack .= '/'; + } + $unpack .= $format . $name; + + if ($format === 'C') { + ++$length; + } else if ($format === 'n') { + $length += 2; + } else if ($format === 'N') { + $length += 4; + } else { + throw new InvalidArgumentException('Invalid format given'); + } + } + + return $this->readLength($length)->then(function ($response) use ($unpack) { + return unpack($unpack, $response); + }); + } + + public function readLength($bytes) + { + $deferred = new Deferred(); + + $this->readBufferCallback(function (&$buffer) use ($bytes, $deferred) { + if (strlen($buffer) >= $bytes) { + $deferred->resolve((string)substr($buffer, 0, $bytes)); + $buffer = (string)substr($buffer, $bytes); + + return StreamReader::RET_DONE; + } + }); + + return $deferred->promise(); + } + + public function readByte() + { + return $this->readBinary(array( + 'byte' => 'C' + ))->then(function ($data) { + return $data['byte']; + }); + } + + public function readByteAssert($expect) + { + return $this->readByte()->then(function ($byte) use ($expect) { + if ($byte !== $expect) { + throw new UnexpectedValueException('Unexpected byte encountered'); + } + return $byte; + }); + } + + public function readStringNull() + { + $deferred = new Deferred(); + $string = ''; + + $that = $this; + $readOne = function () use (&$readOne, $that, $deferred, &$string) { + $that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) { + if ($byte === 0x00) { + $deferred->resolve($string); + } else { + $string .= chr($byte); + $readOne(); + } + }); + }; + $readOne(); + + return $deferred->promise(); + } + + public function readBufferCallback(/* callable */ $callable) + { + if (!is_callable($callable)) { + throw new InvalidArgumentException('Given function must be callable'); + } + + if ($this->queue) { + $this->queue []= $callable; + } else { + $this->queue = array($callable); + + if ($this->buffer !== '') { + // this is the first element in the queue and the buffer is filled => trigger write procedure + $this->write(''); + } + } + } + + public function getBuffer() + { + return $this->buffer; + } +} |