├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── Socks ├── Client.php ├── Connector.php ├── Factory.php ├── Server.php └── StreamReader.php ├── composer.json ├── examples ├── client.php ├── server-middleman.php └── server.php ├── license.txt ├── phpunit.xml.dist └── tests ├── ClientApiTest.php ├── FactoryTest.php ├── PairTest.php ├── ServerApiTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | /composer.phar 4 | /build 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.3 5 | before_script: 6 | - composer install --dev --dev --prefer-source --no-interaction 7 | script: 8 | - phpunit --coverage-text 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | This file is a manually maintained list of changes for each release. Feel free 4 | to add your changes here when sending pull requests. Also send corrections if 5 | you spot any mistakes. 6 | 7 | ## 0.4.0 (2013-05-24) 8 | 9 | * BC break: Update react to current v0.3 and thus also replace `ConnectionManager` with `Connector` 10 | * BC break: New `Client::createConnector()` replaces inheriting `ConnectionManagerInterface` 11 | * BC break: New `Client::createSecureConnector()` replaces `Client::createSecureConnectionManager()` 12 | 13 | ## 0.3.1 (2012-12-29) 14 | 15 | * Fix: Server event logging 16 | * Fix: Closing invalid connections 17 | 18 | ## 0.3.0 (2012-12-23) 19 | 20 | * Feature: Add async `Server` implementation 21 | 22 | ## 0.2.0 (2012-12-03) 23 | 24 | * BC break: Whole new API, now using async patterns based on react/react 25 | * BC break: Re-organize into `Socks` namespace 26 | * Feature: Whole new async API: `PromiseInterface Client::getConnection(string $hostname, int $port)` 27 | * Feature: SOCKS5 username/password authentication: `Client::setAuth(string $username, string $password)` 28 | * Feature: SOCKS4a/SOCKS5 support local *and* remote resolving: `Client::setResolveLocal(boolean $resolveLocal)` 29 | * Feature: SOCKS protocol can now be switched during runtime: `Client::setProtocolVersion(string $version)` 30 | * Feature: Simple interface for HTTP over SOCKS: `HttpClient Client::createHttpClient()` 31 | * Feature: Simple interface for SSL/TLS over SOCKS: `Client` now implements `ConnectionManagerInterface` 32 | * Feature: Simple interface for TCP over SOCKS: `SecureConnectionManager Client::createSecureConnectionManager()` 33 | 34 | ## 0.1.0 (2011-05-16) 35 | 36 | * First tagged release 37 | * Simple, blocking API: `resource Socks::connect(string $hostname, int $port)` 38 | * Support for SOCKS4, SOCKS4a, SOCKS5 and hostname, IPv4 and IPv6 addressing 39 | 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MAINTENANCE MODE 2 | 3 | This repository is currently in the process of being split up into a simple blocking client implementation and an async non-blocking client and server implementation. See [issue #2](https://github.com/clue/php-socks/issues/2) for details. 4 | 5 | * If you're already using this library in legacy v0.4, 6 | consider upgrading to [clue/socks-react](https://github.com/clue/php-socks-react). 7 | Upgrading should take no longer than 10 minutes, see the CHANGELOG.md for details. 8 | 9 | * If you're looking for an async non-blocking client and server implementation, 10 | head over to [clue/socks-react](https://github.com/clue/php-socks-react). 11 | 12 | * If you're looking for the simple blocking client implementation, 13 | your best bet is to wait for [issue #2](https://github.com/clue/php-socks/issues/2) to be completed. Feel like contributing? :) 14 | 15 | The following description applies to legacy v0.4, which has already been migrated to [clue/socks-react](https://github.com/clue/php-socks-react). 16 | 17 | # clue/socks - SOCKS client and server [![Build Status](https://travis-ci.org/clue/php-socks.svg?branch=master)](https://travis-ci.org/clue/php-socks) 18 | 19 | Async SOCKS client library to connect to SOCKS4, SOCKS4a and SOCKS5 proxy servers, 20 | as well as a SOCKS server implementation, capable of handling multiple concurrent 21 | connections in a non-blocking fashion. 22 | 23 | ## Description 24 | 25 | The SOCKS protocol family can be used to easily tunnel TCP connections independent 26 | of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet, etc. 27 | 28 | ## Quickstart examples 29 | 30 | Once [installed](#install), initialize a connection to a remote SOCKS proxy server: 31 | 32 | ```PHP 33 | createCached('8.8.8.8', $loop); 41 | 42 | // create SOCKS client which communicates with SOCKS server 127.0.0.1:9050 43 | $factory = new Socks\Factory($loop, $dns); 44 | $client = $factory->createClient('127.0.0.1', 9050); 45 | 46 | // now work with your $client, see below 47 | 48 | $loop->run(); 49 | ``` 50 | 51 | ### Tunnelled TCP connections 52 | 53 | The `Socks/Client` uses a [Promise](https://github.com/reactphp/promise)-based interface which makes working with asynchronous functions a breeze. 54 | Let's open up a TCP [Stream](https://github.com/reactphp/stream) connection and write some data: 55 | ```PHP 56 | $tcp = $client->createConnector(); 57 | 58 | $tcp->create('www.google.com',80)->then(function (React\Stream\Stream $stream) { 59 | echo 'connected to www.google.com:80'; 60 | $stream->write("GET / HTTP/1.0\r\n\r\n"); 61 | // ... 62 | }); 63 | ``` 64 | 65 | ### HTTP requests 66 | 67 | Or if all you want to do is HTTP requests, `Socks/Client` provides an even simpler [HTTP client](https://github.com/reactphp/http-client) interface: 68 | ```PHP 69 | $httpclient = $client->createHttpClient(); 70 | 71 | $request = $httpclient->request('GET', 'https://www.google.com/', array('user-agent'=>'Custom/1.0')); 72 | $request->on('response', function (React\HttpClient\Response $response) { 73 | var_dump('Headers received:', $response->getHeaders()); 74 | 75 | // dump whole response body 76 | $response->on('data', function ($data) { 77 | echo $data; 78 | }); 79 | }); 80 | $request->end(); 81 | ``` 82 | Yes, this works for both plain HTTP and SSL encrypted HTTPS requests. 83 | 84 | ### SSL/TLS encrypted 85 | 86 | If you want to connect to arbitrary SSL/TLS servers, there sure too is an easy to use API available: 87 | ```PHP 88 | $ssl = $client->createSecureConnector(); 89 | 90 | // now create an SSL encrypted connection (notice the $ssl instead of $tcp) 91 | $ssl->create('www.google.com',443)->then(function (React\Stream\Stream $stream) { 92 | // proceed with just the plain text data and everything is encrypted/decrypted automatically 93 | echo 'connected to SSL encrypted www.google.com'; 94 | $stream->write("GET / HTTP/1.0\r\n\r\n"); 95 | // ... 96 | }); 97 | ``` 98 | 99 | ## SOCKS Protocol versions & differences 100 | 101 | While SOCKS4 already had (a somewhat limited) support for `SOCKS BIND` requests and SOCKS5 added generic UDP support (`SOCKS UDPASSOCIATE`), this library focuses on the most commonly used core feature of `SOCKS CONNECT`. In this mode, a SOCKS server acts as a generic proxy allowing higher level application protocols to work through it. 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 |
SOCKS4SOCKS4aSOCKS5
Protocol specificationSOCKS4.protocolSOCKS4A.protocolRFC 1928
Tunnel outgoing TCP connections
Remote DNS resolving
IPv6 addresses
Username/Password authentication✓ (as per RFC 1929)
Handshake # roundtrips112 (3 with authentication)
Handshake traffic
+ remote DNS
17 bytes
17 bytes
+ hostname + 1
variable (+ auth + IPv6)
+ hostname - 3
153 | 154 | Note, this is __not__ a full SOCKS5 implementation due to missing GSSAPI authentication (but it's unlikely you're going to miss it anyway). 155 | 156 | ### Explicitly setting protocol version 157 | 158 | This library supports the SOCKS4, SOCKS4a and SOCKS5 protocol versions. 159 | Usually, there's no need to worry about which protocol version is being used. 160 | Depending on which features you use (e.g. [remote DNS resolving](#remote-vs-local-dns-resolving) and [authentication](#username--password-authentication)), the `Socks/Client` automatically uses the _best_ protocol available. In general this library automatically switches to higher protocol versions when needed, but tries to keep things simple otherwise and sticks to lower protocol versions when possible. 161 | The `Socks/Server` supports all protocol versions by default. 162 | 163 | If want to explicitly set the protocol version, use the supported values `4`, `4a` or `5`: 164 | 165 | ```PHP 166 | // valid protocol versions: 167 | $client->setProtocolVersion('4a'); 168 | $server->setProtocolVersion(5); 169 | ``` 170 | 171 | In order to reset the protocol version to its default (i.e. automatic detection), use `null` as protocol version. 172 | 173 | ```PHP 174 | $client->setProtocolVersion(null); 175 | $server->setProtocolVersion(null); 176 | ``` 177 | 178 | ### Remote vs. local DNS resolving 179 | 180 | By default, the `Socks/Client` uses local DNS resolving to resolve target hostnames 181 | into IP addresses and only transmits the resulting target IP to the socks server. 182 | 183 | Resolving locally usually results in better performance as for each outgoing 184 | request both resolving the hostname and initializing the connection to the 185 | SOCKS server can be done simultanously. So by the time the SOCKS connection is 186 | established (requires a TCP handshake for each connection), the target hostname 187 | will likely already be resolved ( _usually_ either already cached or requires a simple DNS query via UDP). 188 | 189 | You may want to switch to remote DNS resolving if your local `Socks/Client` either can not 190 | resolve target hostnames because it has no direct access to the internet or if 191 | it should not resolve target hostnames because its outgoing DNS traffic might 192 | be intercepted (in particular when using the [Tor network](#using-the-tor-anonymity-network-to-tunnel-socks-connections)). 193 | 194 | Local DNS resolving is available in all SOCKS protocol versions. 195 | Remote DNS resolving is only available for SOCKS4a and SOCKS5 (i.e. it is NOT available for SOCKS4). 196 | 197 | Valid values are boolean `true`(default) or `false`. 198 | 199 | ```PHP 200 | $client->setResolveLocal(false); 201 | ``` 202 | 203 | ### Username / Password authentication 204 | 205 | This library supports username/password authentication for SOCKS5 servers as defined in [RFC 1929](http://tools.ietf.org/html/rfc1929). 206 | 207 | On the client side, simply set your username and password to use for authentication (see below). 208 | 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. 209 | 210 | 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. 211 | Authentication is only supported by protocol version 5 (SOCKS5), so setting authentication on the `Socks/Client` enforces communication with protocol version 5 and complains if you have explicitly set anything else. 212 | 213 | ```PHP 214 | $client->setAuth('username', 'password'); 215 | ``` 216 | 217 | Setting authentication on the `Socks/Server` enforces each further connected client to use protocol version 5. 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. 218 | 219 | Because your authentication mechanism might take some time to actually check the provided authentication credentials (like querying a remote database or webservice), the server side uses a [Promise](https://github.com/reactphp/promise) based interface. While this might seem complex at first, it actually provides a very simple way to handle simultanous connections in a non-blocking fashion and increases overall performance. 220 | 221 | ```PHP 222 | $server->setAuth(function ($username, $password) { 223 | // either return a boolean success value right away or use promises for delayed authentication 224 | }); 225 | ``` 226 | 227 | Or if you only accept static authentication details, you can use the simple array-based authentication method as a shortcut: 228 | 229 | ```PHP 230 | $server->setAuthArray(array( 231 | 'tom' => 'password', 232 | 'admin' => 'root' 233 | )); 234 | ``` 235 | 236 | If you do not want to use authentication anymore: 237 | 238 | ```PHP 239 | $client->unsetAuth(); 240 | $server->unsetAuth(); 241 | ``` 242 | 243 | ## Usage 244 | 245 | ### Using SSH as a SOCKS server 246 | 247 | 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`) by issuing: 248 | 249 | `$ ssh -D 9050 ssh-server` 250 | 251 | ```PHP 252 | $client = $factory->createClient('127.0.0.1', 9050); 253 | ``` 254 | 255 | ### Using the Tor (anonymity network) to tunnel SOCKS connections 256 | 257 | The [Tor anonymity network](http://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 SOCKS4 and SOCKS5 interface on TCP port 9050 by default which allows you to tunnel any traffic through the anonymity network. In most scenarios you probably 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. 258 | 259 | ```PHP 260 | 261 | $client = $factory->createClient('127.0.0.1', 9050); 262 | $client->setResolveLocal(false); 263 | ``` 264 | 265 | ## Install 266 | 267 | The recommended way to install this library is [through composer](http://getcomposer.org). [New to composer?](http://getcomposer.org/doc/00-intro.md) 268 | 269 | ```JSON 270 | { 271 | "require": { 272 | "clue/Socks": "0.4.*" 273 | } 274 | } 275 | ``` 276 | 277 | ## License 278 | 279 | MIT, see license.txt 280 | -------------------------------------------------------------------------------- /Socks/Client.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 52 | $this->connector = $connector; 53 | $this->socksHost = $socksHost; 54 | $this->socksPort = $socksPort; 55 | $this->resolver = $resolver; 56 | $this->timeout = ini_get("default_socket_timeout"); 57 | } 58 | 59 | public function setTimeout($timeout) 60 | { 61 | $this->timeout = $timeout; 62 | } 63 | 64 | public function setResolveLocal($resolveLocal) 65 | { 66 | if ($this->protocolVersion === '4' && !$resolveLocal) { 67 | throw new UnexpectedValueException('SOCKS4 requires resolving locally. Consider using another protocol version or resolving locally'); 68 | } 69 | $this->resolveLocal = $resolveLocal; 70 | } 71 | 72 | public function setProtocolVersion($version) 73 | { 74 | if ($version !== null) { 75 | $version = (string)$version; 76 | if (!in_array($version, array('4', '4a', '5'), true)) { 77 | throw new InvalidArgumentException('Invalid protocol version given'); 78 | } 79 | if ($version !== '5' && $this->auth){ 80 | throw new UnexpectedValueException('Unable to change protocol version to anything but SOCKS5 while authentication is used. Consider removing authentication info or sticking to SOCKS5'); 81 | } 82 | if ($version === '4' && !$this->resolveLocal) { 83 | throw new UnexpectedValueException('Unable to change to SOCKS4 while resolving locally is turned off. Consider using another protocol version or resolving locally'); 84 | } 85 | } 86 | $this->protocolVersion = $version; 87 | } 88 | 89 | /** 90 | * set login data for username/password authentication method (RFC1929) 91 | * 92 | * @param string $username 93 | * @param string $password 94 | * @link http://tools.ietf.org/html/rfc1929 95 | */ 96 | public function setAuth($username, $password) 97 | { 98 | if (strlen($username) > 255 || strlen($password) > 255) { 99 | throw new InvalidArgumentException('Both username and password MUST NOT exceed a length of 255 bytes each'); 100 | } 101 | if ($this->protocolVersion !== null && $this->protocolVersion !== '5') { 102 | throw new UnexpectedValueException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication'); 103 | } 104 | $this->auth = pack('C2', 0x01, strlen($username)) . $username . pack('C', strlen($password)) . $password; 105 | } 106 | 107 | public function unsetAuth() 108 | { 109 | $this->auth = null; 110 | } 111 | 112 | public function createHttpClient() 113 | { 114 | return new HttpClient($this->loop, $this->createConnector(), $this->createSecureConnector()); 115 | } 116 | 117 | public function createSecureConnector() 118 | { 119 | return new SecureConnector($this->createConnector(), $this->loop); 120 | } 121 | 122 | public function createConnector() 123 | { 124 | return new Connector($this); 125 | } 126 | 127 | public function getConnection($host, $port) 128 | { 129 | if (strlen($host) > 255 || $port > 65535 || $port < 0) { 130 | return When::reject(new InvalidArgumentException('Invalid target specified')); 131 | } 132 | $deferred = new Deferred(); 133 | 134 | $timestampTimeout = microtime(true) + $this->timeout; 135 | $timerTimeout = $this->loop->addTimer($this->timeout, function () use ($deferred) { 136 | $deferred->reject(new Exception('Timeout while connecting to socks server')); 137 | // TODO: stop initiating connection and DNS query 138 | }); 139 | 140 | // create local references as these settings may change later due to its async nature 141 | $auth = $this->auth; 142 | $protocolVersion = $this->protocolVersion; 143 | 144 | // protocol version not explicitly set? 145 | if ($protocolVersion === null) { 146 | // authentication requires SOCKS5, otherwise use SOCKS4a 147 | $protocolVersion = ($auth === null) ? '4a' : '5'; 148 | } 149 | 150 | $loop = $this->loop; 151 | $that = $this; 152 | When::all( 153 | array( 154 | $this->connector->create($this->socksHost, $this->socksPort)->then( 155 | null, 156 | function ($error) { 157 | throw new Exception('Unable to connect to socks server', 0, $error); 158 | } 159 | ), 160 | $this->resolve($host)->then( 161 | null, 162 | function ($error) { 163 | throw new Exception('Unable to resolve remote hostname', 0, $error); 164 | } 165 | ) 166 | ), 167 | function ($fulfilled) use ($deferred, $port, $timestampTimeout, $that, $loop, $timerTimeout, $protocolVersion, $auth) { 168 | $loop->cancelTimer($timerTimeout); 169 | 170 | $timeout = max($timestampTimeout - microtime(true), 0.1); 171 | $deferred->resolve($that->handleConnectedSocks($fulfilled[0], $fulfilled[1], $port, $timeout, $protocolVersion, $auth)); 172 | }, 173 | function ($error) use ($deferred, $loop, $timerTimeout) { 174 | $loop->cancelTimer($timerTimeout); 175 | $deferred->reject(new Exception('Unable to connect to socks server', 0, $error)); 176 | } 177 | ); 178 | return $deferred->promise(); 179 | } 180 | 181 | private function resolve($host) 182 | { 183 | // return if it's already an IP or we want to resolve remotely (socks 4 only supports resolving locally) 184 | if (false !== filter_var($host, FILTER_VALIDATE_IP) || ($this->protocolVersion !== '4' && !$this->resolveLocal)) { 185 | return When::resolve($host); 186 | } 187 | 188 | return $this->resolver->resolve($host); 189 | } 190 | 191 | public function handleConnectedSocks(Stream $stream, $host, $port, $timeout, $protocolVersion, $auth=null) 192 | { 193 | $deferred = new Deferred(); 194 | $resolver = $deferred->resolver(); 195 | 196 | $timerTimeout = $this->loop->addTimer($timeout, function () use ($resolver) { 197 | $resolver->reject(new Exception('Timeout while establishing socks session')); 198 | }); 199 | 200 | if ($protocolVersion === '5' || $auth !== null) { 201 | $promise = $this->handleSocks5($stream, $host, $port, $auth); 202 | } else { 203 | $promise = $this->handleSocks4($stream, $host, $port); 204 | } 205 | $promise->then(function () use ($resolver, $stream) { 206 | $resolver->resolve($stream); 207 | }, function($error) use ($resolver) { 208 | $resolver->reject(new Exception('Unable to communicate...', 0, $error)); 209 | }); 210 | 211 | $loop = $this->loop; 212 | $deferred->then( 213 | function (Stream $stream) use ($timerTimeout, $loop) { 214 | $loop->cancelTimer($timerTimeout); 215 | $stream->removeAllListeners('end'); 216 | return $stream; 217 | }, 218 | function ($error) use ($stream, $timerTimeout, $loop) { 219 | $loop->cancelTimer($timerTimeout); 220 | $stream->close(); 221 | return $error; 222 | } 223 | ); 224 | 225 | $stream->on('end', function (Stream $stream) use ($resolver) { 226 | $resolver->reject(new Exception('Premature end while establishing socks session')); 227 | }); 228 | 229 | return $deferred->promise(); 230 | } 231 | 232 | protected function handleSocks4($stream, $host, $port) 233 | { 234 | // do not resolve hostname. only try to convert to IP 235 | $ip = ip2long($host); 236 | 237 | // send IP or (0.0.0.1) if invalid 238 | $data = pack('C2nNC', 0x04, 0x01, $port, $ip === false ? 1 : $ip, 0x00); 239 | 240 | if ($ip === false) { 241 | // host is not a valid IP => send along hostname (SOCKS4a) 242 | $data .= $host . pack('C', 0x00); 243 | } 244 | 245 | $stream->write($data); 246 | 247 | $reader = new StreamReader($stream); 248 | return $reader->readBinary(array( 249 | 'null' => 'C', 250 | 'status' => 'C', 251 | 'port' => 'n', 252 | 'ip' => 'N' 253 | ))->then(function ($data) { 254 | if ($data['null'] !== 0x00 || $data['status'] !== 0x5a) { 255 | throw new Exception('Invalid SOCKS response'); 256 | } 257 | }); 258 | } 259 | 260 | protected function handleSocks5(Stream $stream, $host, $port, $auth=null) 261 | { 262 | // protocol version 5 263 | $data = pack('C', 0x05); 264 | if ($auth === null) { 265 | // one method, no authentication 266 | $data .= pack('C2', 0x01, 0x00); 267 | } else { 268 | // two methods, username/password and no authentication 269 | $data .= pack('C3', 0x02, 0x02, 0x00); 270 | } 271 | $stream->write($data); 272 | 273 | $that = $this; 274 | $reader = new StreamReader($stream); 275 | return $reader->readBinary(array( 276 | 'version' => 'C', 277 | 'method' => 'C' 278 | ))->then(function ($data) use ($auth, $stream, $reader) { 279 | if ($data['version'] !== 0x05) { 280 | throw new Exception('Version/Protocol mismatch'); 281 | } 282 | 283 | if ($data['method'] === 0x02 && $auth !== null) { 284 | // username/password authentication requested and provided 285 | $stream->write($auth); 286 | 287 | return $reader->readBinary(array( 288 | 'version' => 'C', 289 | 'status' => 'C' 290 | ))->then(function ($data) { 291 | if ($data['version'] !== 0x01 || $data['status'] !== 0x00) { 292 | throw new Exception('Username/Password authentication failed'); 293 | } 294 | }); 295 | } else if ($data['method'] !== 0x00) { 296 | // any other method than "no authentication" 297 | throw new Exception('Unacceptable authentication method requested'); 298 | } 299 | })->then(function () use ($stream, $reader, $host, $port) { 300 | // do not resolve hostname. only try to convert to (binary/packed) IP 301 | $ip = @inet_pton($host); 302 | 303 | $data = pack('C3', 0x05, 0x01, 0x00); 304 | if ($ip === false) { 305 | // not an IP, send as hostname 306 | $data .= pack('C2', 0x03, strlen($host)) . $host; 307 | } else { 308 | // send as IPv4 / IPv6 309 | $data .= pack('C', (strpos($host, ':') === false) ? 0x01 : 0x04) . $ip; 310 | } 311 | $data .= pack('n', $port); 312 | 313 | $stream->write($data); 314 | 315 | return $reader->readBinary(array( 316 | 'version' => 'C', 317 | 'status' => 'C', 318 | 'null' => 'C', 319 | 'type' => 'C' 320 | )); 321 | })->then(function ($data) use ($reader) { 322 | if ($data['version'] !== 0x05 || $data['status'] !== 0x00 || $data['null'] !== 0x00) { 323 | throw new Exception('Invalid SOCKS response'); 324 | } 325 | if ($data['type'] === 0x01) { 326 | // IPv4 address => skip IP and port 327 | return $reader->readLength(6); 328 | } else if ($data['type'] === 0x03) { 329 | // domain name => read domain name length 330 | return $reader->readBinary(array( 331 | 'length' => 'C' 332 | ))->then(function ($data) use ($that) { 333 | // skip domain name and port 334 | return $that->readLength($data['length'] + 2); 335 | }); 336 | } else if ($data['type'] === 0x04) { 337 | // IPv6 address => skip IP and port 338 | return $reader->readLength(18); 339 | } else { 340 | throw new Exception('Invalid SOCKS reponse: Invalid address type'); 341 | } 342 | }); 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /Socks/Connector.php: -------------------------------------------------------------------------------- 1 | client = $socksClient; 15 | } 16 | 17 | public function create($host, $port) 18 | { 19 | return $this->client->getConnection($host, $port); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Socks/Factory.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 14 | $this->resolver = $resolver; 15 | } 16 | 17 | public function createClient($socksHost, $socksPort) 18 | { 19 | $connector = $this->createConnector(); 20 | return new Client($this->loop, $connector, $this->resolver, $socksHost, $socksPort); 21 | } 22 | 23 | public function createServer($socket) 24 | { 25 | $connector = $this->createConnector(); 26 | return new Server($socket, $this->loop, $connector); 27 | } 28 | 29 | protected function createConnector() 30 | { 31 | return new Connector($this->loop, $this->resolver); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Socks/Server.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 30 | $this->connector = $connector; 31 | 32 | $that = $this; 33 | $serverInterface->on('connection', function ($connection) use ($that) { 34 | $that->emit('connection', array($connection)); 35 | $that->onConnection($connection); 36 | }); 37 | } 38 | 39 | public function setProtocolVersion($version) 40 | { 41 | if ($version !== null) { 42 | $version = (string)$version; 43 | if (!in_array($version, array('4', '4a', '5'), true)) { 44 | throw new InvalidArgumentException('Invalid protocol version given'); 45 | } 46 | if ($version !== '5' && $this->auth !== null){ 47 | throw new UnexpectedValueException('Unable to change protocol version to anything but SOCKS5 while authentication is used. Consider removing authentication info or sticking to SOCKS5'); 48 | } 49 | } 50 | $this->protocolVersion = $version; 51 | } 52 | 53 | public function setAuth($auth) 54 | { 55 | if (!is_callable($auth)) { 56 | throw new InvalidArgumentException('Given authenticator is not a valid callable'); 57 | } 58 | if ($this->protocolVersion !== null && $this->protocolVersion !== '5') { 59 | throw new UnexpectedValueException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication'); 60 | } 61 | // wrap authentication callback in order to cast its return value to a promise 62 | $this->auth = function($username, $password) use ($auth) { 63 | $ret = call_user_func($auth, $username, $password); 64 | if ($ret instanceof PromiseInterface) { 65 | return $ret; 66 | } 67 | return $ret ? When::resolve() : When::reject(); 68 | }; 69 | } 70 | 71 | public function setAuthArray(array $login) 72 | { 73 | $this->setAuth(function ($username, $password) use ($login) { 74 | return (isset($login[$username]) && (string)$login[$username] === $password); 75 | }); 76 | } 77 | 78 | public function unsetAuth() 79 | { 80 | $this->auth = null; 81 | } 82 | 83 | public function onConnection(Connection $connection) 84 | { 85 | $that = $this; 86 | $this->handleSocks($connection)->then(function($remote) use ($connection){ 87 | $connection->emit('ready',array($remote)); 88 | }, function ($error) use ($connection, $that) { 89 | if (!($error instanceof \Exception)) { 90 | $error = new \Exception($error); 91 | } 92 | $connection->emit('error', array($error)); 93 | $that->endConnection($connection); 94 | }); 95 | } 96 | 97 | /** 98 | * gracefully shutdown connection by flushing all remaining data and closing stream 99 | * 100 | * @param Stream $stream 101 | */ 102 | public function endConnection(Stream $stream) 103 | { 104 | $tid = true; 105 | $loop = $this->loop; 106 | 107 | // cancel below timer in case connection is closed in time 108 | $stream->once('close', function () use (&$tid, $loop) { 109 | // close event called before the timer was set up, so everything is okay 110 | if ($tid === true) { 111 | // make sure to not start a useless timer 112 | $tid = false; 113 | } else { 114 | $loop->cancelTimer($tid); 115 | } 116 | }); 117 | 118 | // shut down connection by pausing input data, flushing outgoing buffer and then exit 119 | $stream->pause(); 120 | $stream->end(); 121 | 122 | // check if connection is not already closed 123 | if ($tid === true) { 124 | // fall back to forcefully close connection in 3 seconds if buffer can not be flushed 125 | $tid = $loop->addTimer(3.0, array($stream,'close')); 126 | } 127 | } 128 | 129 | private function handleSocks(Stream $stream) 130 | { 131 | $reader = new StreamReader($stream); 132 | $that = $this; 133 | 134 | $auth = $this->auth; 135 | $protocolVersion = $this->protocolVersion; 136 | 137 | // authentication requires SOCKS5 138 | if ($auth !== null) { 139 | $protocolVersion = '5'; 140 | } 141 | 142 | return $reader->readByte()->then(function ($version) use ($stream, $that, $protocolVersion, $auth){ 143 | if ($version === 0x04) { 144 | if ($protocolVersion === '5') { 145 | throw new UnexpectedValueException('SOCKS4 not allowed due to configuration'); 146 | } 147 | return $that->handleSocks4($stream, $protocolVersion); 148 | } else if ($version === 0x05) { 149 | if ($protocolVersion !== null && $protocolVersion !== '5') { 150 | throw new UnexpectedValueException('SOCKS5 not allowed due to configuration'); 151 | } 152 | return $that->handleSocks5($stream, $auth); 153 | } 154 | throw new UnexpectedValueException('Unexpected/unknown version number'); 155 | }); 156 | } 157 | 158 | public function handleSocks4(Stream $stream, $protocolVersion) 159 | { 160 | // suppliying hostnames is only allowed for SOCKS4a (or automatically detected version) 161 | $supportsHostname = ($protocolVersion === null || $protocolVersion === '4a'); 162 | 163 | $reader = new StreamReader($stream); 164 | $that = $this; 165 | return $reader->readByteAssert(0x01)->then(function () use ($reader) { 166 | return $reader->readBinary(array( 167 | 'port' => 'n', 168 | 'ipLong' => 'N', 169 | 'null' => 'C' 170 | )); 171 | })->then(function ($data) use ($reader, $supportsHostname) { 172 | if ($data['null'] !== 0x00) { 173 | throw new Exception('Not a null byte'); 174 | } 175 | if ($data['ipLong'] === 0) { 176 | throw new Exception('Invalid IP'); 177 | } 178 | if ($data['port'] === 0) { 179 | throw new Exception('Invalid port'); 180 | } 181 | if ($data['ipLong'] < 256 && $supportsHostname) { 182 | // invalid IP => probably a SOCKS4a request which appends the hostname 183 | return $reader->readStringNull()->then(function ($string) use ($data){ 184 | return array($string, $data['port']); 185 | }); 186 | } else { 187 | $ip = long2ip($data['ipLong']); 188 | return array($ip, $data['port']); 189 | } 190 | })->then(function ($target) use ($stream, $that) { 191 | return $that->connectTarget($stream, $target)->then(function (Stream $remote) use ($stream){ 192 | $stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); 193 | 194 | return $remote; 195 | }, function($error) use ($stream){ 196 | $stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); 197 | 198 | throw $error; 199 | }); 200 | }, function($error) { 201 | throw new UnexpectedValueException('SOCKS4 protocol error',0,$error); 202 | }); 203 | } 204 | 205 | public function handleSocks5(Stream $stream, $auth=null) 206 | { 207 | $reader = new StreamReader($stream); 208 | $that = $this; 209 | return $reader->readByte()->then(function ($num) use ($reader) { 210 | // $num different authentication mechanisms offered 211 | return $reader->readLength($num); 212 | })->then(function ($methods) use ($reader, $stream, $auth) { 213 | if ($auth === null && strpos($methods,"\x00") !== false) { 214 | // accept "no authentication" 215 | $stream->write(pack('C2', 0x05, 0x00)); 216 | return 0x00; 217 | } else if ($auth !== null && strpos($methods,"\x02") !== false) { 218 | // username/password authentication (RFC 1929) sub negotiation 219 | $stream->write(pack('C2', 0x05, 0x02)); 220 | return $reader->readByteAssert(0x01)->then(function () use ($reader) { 221 | return $reader->readByte(); 222 | })->then(function ($length) use ($reader) { 223 | return $reader->readLength($length); 224 | })->then(function ($username) use ($reader, $auth, $stream) { 225 | return $reader->readByte()->then(function ($length) use ($reader) { 226 | return $reader->readLength($length); 227 | })->then(function ($password) use ($username, $auth, $stream) { 228 | // username and password known => authenticate 229 | // echo 'auth: ' . $username.' : ' . $password . PHP_EOL; 230 | return $auth($username, $password)->then(function () use ($stream, $username) { 231 | // accept 232 | $stream->emit('auth', array($username)); 233 | $stream->write(pack('C2', 0x01, 0x00)); 234 | }, function() use ($stream) { 235 | // reject => send any code but 0x00 236 | $stream->end(pack('C2', 0x01, 0xFF)); 237 | throw new UnexpectedValueException('Unable to authenticate'); 238 | }); 239 | }); 240 | }); 241 | } else { 242 | // reject all offered authentication methods 243 | $stream->end(pack('C2', 0x05, 0xFF)); 244 | throw new UnexpectedValueException('No acceptable authentication mechanism found'); 245 | } 246 | })->then(function ($method) use ($reader, $stream) { 247 | return $reader->readBinary(array( 248 | 'version' => 'C', 249 | 'command' => 'C', 250 | 'null' => 'C', 251 | 'type' => 'C' 252 | )); 253 | })->then(function ($data) use ($reader) { 254 | if ($data['version'] !== 0x05) { 255 | throw new UnexpectedValueException('Invalid SOCKS version'); 256 | } 257 | if ($data['command'] !== 0x01) { 258 | throw new UnexpectedValueException('Only CONNECT requests supported'); 259 | } 260 | // if ($data['null'] !== 0x00) { 261 | // throw new UnexpectedValueException('Reserved byte has to be NULL'); 262 | // } 263 | if ($data['type'] === 0x03) { 264 | // target hostname string 265 | return $reader->readByte()->then(function ($len) use ($reader) { 266 | return $reader->readLength($len); 267 | }); 268 | } else if ($data['type'] === 0x01) { 269 | // target IPv4 270 | return $reader->readLength(4)->then(function ($addr) { 271 | return inet_ntop($addr); 272 | }); 273 | } else if ($data['type'] === 0x04) { 274 | // target IPv6 275 | return $reader->readLength(16)->then(function ($addr) { 276 | return inet_ntop($addr); 277 | }); 278 | } else { 279 | throw new UnexpectedValueException('Invalid target type'); 280 | } 281 | })->then(function ($host) use ($reader) { 282 | return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host) { 283 | return array($host, $data['port']); 284 | }); 285 | })->then(function ($target) use ($that, $stream) { 286 | return $that->connectTarget($stream, $target); 287 | }, function($error) use ($stream) { 288 | throw new UnexpectedValueException('SOCKS5 protocol error',0,$error); 289 | })->then(function (Stream $remote) use ($stream) { 290 | $stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0)); 291 | 292 | return $remote; 293 | }, function(Exception $error) use ($stream){ 294 | $code = 0x01; 295 | $stream->end(pack('C4Nn', 0x05, $code, 0x00, 0x01, 0, 0)); 296 | 297 | throw $error; 298 | }); 299 | } 300 | 301 | public function connectTarget(Stream $stream, array $target) 302 | { 303 | $stream->emit('target', $target); 304 | $that = $this; 305 | return $this->connector->create($target[0], $target[1])->then(function (Stream $remote) use ($stream, $that) { 306 | if (!$stream->isWritable()) { 307 | $remote->close(); 308 | throw new UnexpectedValueException('Remote connection successfully established after client connection closed'); 309 | } 310 | 311 | $stream->pipe($remote, array('end'=>false)); 312 | $remote->pipe($stream, array('end'=>false)); 313 | 314 | // remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local 315 | $remote->on('end', function() use ($stream, $that) { 316 | $stream->emit('shutdown', array('remote', null)); 317 | $that->endConnection($stream); 318 | }); 319 | 320 | // local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote 321 | $stream->on('end', function() use ($remote, $that) { 322 | $that->endConnection($remote); 323 | }); 324 | 325 | // set bigger buffer size of 100k to improve performance 326 | $stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024; 327 | 328 | return $remote; 329 | }, function(Exception $error) { 330 | throw new UnexpectedValueException('Unable to connect to remote target', 0, $error); 331 | }); 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /Socks/StreamReader.php: -------------------------------------------------------------------------------- 1 | stream = $stream; 17 | } 18 | 19 | public function readBinary($structure) 20 | { 21 | $length = 0; 22 | $unpack = ''; 23 | foreach ($structure as $name=>$format) { 24 | if ($length !== 0) { 25 | $unpack .= '/'; 26 | } 27 | $unpack .= $format . $name; 28 | 29 | if ($format === 'C') { 30 | ++$length; 31 | } else if ($format === 'n') { 32 | $length += 2; 33 | } else if ($format === 'N') { 34 | $length += 4; 35 | } else { 36 | throw new InvalidArgumentException('Invalid format given'); 37 | } 38 | } 39 | 40 | return $this->readLength($length)->then(function ($response) use ($unpack) { 41 | return unpack($unpack, $response); 42 | }); 43 | } 44 | 45 | public function readLength($bytes) 46 | { 47 | $deferred = new Deferred(); 48 | $oldsize = $this->stream->bufferSize; 49 | $this->stream->bufferSize = $bytes; 50 | 51 | $buffer = ''; 52 | 53 | $fn = function ($data, Stream $stream) use (&$buffer, &$bytes, $deferred, $oldsize, &$fn) { 54 | $bytes -= strlen($data); 55 | $buffer .= $data; 56 | 57 | $deferred->progress($data); 58 | 59 | if ($bytes === 0) { 60 | $stream->bufferSize = $oldsize; 61 | $stream->removeListener('data', $fn); 62 | 63 | $deferred->resolve($buffer); 64 | } else { 65 | $stream->bufferSize = $bytes; 66 | } 67 | }; 68 | $this->stream->on('data', $fn); 69 | return $deferred->promise(); 70 | } 71 | 72 | public function readByte() 73 | { 74 | return $this->readBinary(array( 75 | 'byte' => 'C' 76 | ))->then(function ($data) { 77 | return $data['byte']; 78 | }); 79 | } 80 | 81 | public function readNull(){ 82 | return $this->readByteAssert(0x00); 83 | } 84 | 85 | public function readByteAssert($expect) 86 | { 87 | return $this->readByte()->then(function ($byte) use ($expect) { 88 | if ($byte !== $expect) { 89 | throw new UnexpectedValueException('Unexpected byte encountered'); 90 | } 91 | return $byte; 92 | }); 93 | } 94 | 95 | public function readChar() 96 | { 97 | return $this->readLength(1); 98 | } 99 | 100 | public function readStringNull() 101 | { 102 | $deferred = new Deferred(); 103 | $string = ''; 104 | 105 | $that = $this; 106 | $readOne = function () use (&$readOne, $that, $deferred, &$string) { 107 | $that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) { 108 | if ($byte === 0x00) { 109 | $deferred->resolve($string); 110 | } else { 111 | $string .= chr($byte); 112 | $readOne(); 113 | } 114 | }); 115 | }; 116 | $readOne(); 117 | 118 | return $deferred->promise(); 119 | } 120 | 121 | public function readAssert($byteSequence) 122 | { 123 | $deferred = new Deferred(); 124 | $pos = 0; 125 | 126 | $that = $this; 127 | $this->readLength(strlen($byteSequence))->then(function ($data) use ($deferred) { 128 | $deferred->resolve($data); 129 | }, null, function ($part) use ($byteSequence, &$pos, $deferred, $that) { 130 | $len = strlen($part); 131 | $expect = substr($byteSequence, $pos, $len); 132 | 133 | if ($part === $expect) { 134 | $pos += $len; 135 | } else { 136 | $deferred->reject(new UnexpectedValueException('Expected "'.$that->escape($expect).'", but got "'.$that->escape($part).'"')); 137 | } 138 | }); 139 | return $deferred->promise(); 140 | } 141 | 142 | public function escape($bytes) 143 | { 144 | $ret = ''; 145 | for ($i = 0, $l = strlen($bytes); $i < $l; ++$i) { 146 | if ($i !== 0) { 147 | $ret .= ' '; 148 | } 149 | $ret .= sprintf('0x%02X', ord($bytes[$i])); 150 | } 151 | return $ret; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/socks", 3 | "type": "library", 4 | "description": "Async SOCKS proxy client and server (SOCKS4, SOCKS4a and SOCKS5)", 5 | "keywords": ["socks client", "socks server", "tcp tunnel", "socks protocol", "async", "react"], 6 | "homepage": "https://github.com/clue/php-socks", 7 | "license": "MIT", 8 | "autoload": { 9 | "psr-0": {"Socks": ""} 10 | }, 11 | "require": { 12 | "react/http-client": "0.3.*", 13 | "react/event-loop": "0.3.*", 14 | "react/socket-client": "0.3.*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/client.php: -------------------------------------------------------------------------------- 1 | createCached('8.8.8.8', $loop); 15 | 16 | $factory = new Socks\Factory($loop, $dns); 17 | 18 | $client = $factory->createClient('127.0.0.1', 9051); 19 | $client->setTimeout(3.0); 20 | $client->setResolveLocal(false); 21 | //$client->setProtocolVersion(5); 22 | // $client->setAuth('test','test'); 23 | 24 | echo 'Demo SOCKS client connecting to SOCKS server 127.0.0.1:9051' . PHP_EOL; 25 | 26 | function ex(Exception $exception=null) 27 | { 28 | if ($exception !== null) { 29 | echo 'message: ' . $exception->getMessage() . PHP_EOL; 30 | while (($exception = $exception->getPrevious())) { 31 | echo 'previous: ' . $exception->getMessage() . PHP_EOL; 32 | } 33 | } 34 | } 35 | 36 | function assertFail(PromiseInterface $promise, $name='end') 37 | { 38 | return $promise->then( 39 | function (Stream $stream) use ($name) { 40 | echo 'FAIL: connection to '.$name.' OK' . PHP_EOL; 41 | $stream->close(); 42 | }, 43 | function (Exception $error) use ($name) { 44 | 45 | echo 'EXPECTED: connection to '.$name.' failed: '; 46 | ex($error); 47 | } 48 | ); 49 | } 50 | 51 | function assertOkay(PromiseInterface $promise, $name='end') 52 | { 53 | return $promise->then( 54 | function ($stream) use ($name) { 55 | echo 'EXPECTED: connection to '.$name.' OK' . PHP_EOL; 56 | $stream->close(); 57 | }, 58 | function (Exception $error) use ($name) { 59 | echo 'FAIL: connection to '.$name.' failed: '; 60 | ex($error); 61 | } 62 | ); 63 | } 64 | 65 | $tcp = $client->createConnector(); 66 | 67 | assertOkay($tcp->create('www.google.com', 80), 'www.google.com:80'); 68 | 69 | assertFail($tcp->create('www.google.commm', 80), 'www.google.commm:80'); 70 | 71 | assertFail($tcp->create('www.google.com', 8080), 'www.google.com:8080'); 72 | 73 | $ssl = $client->createSecureConnector(); 74 | 75 | assertOkay($ssl->create('www.google.com', 443), 'ssl://www.google.com:443'); 76 | 77 | assertFail($ssl->create('www.google.com', 80), 'ssl://www.google.com:80'); 78 | 79 | assertFail($ssl->create('www.google.com', 8080), 'ssl://www.google.com:8080'); 80 | 81 | // $ssl->getConnection('127.0.0.1','443')->then(function (React\Stream $stream) { 82 | // echo 'connected'; 83 | // $stream->write("GET / HTTP/1.0\r\n\r\n"); 84 | // $stream->on('data', function ($data) { 85 | // echo $data; 86 | // }); 87 | // }); 88 | 89 | // $factory = new React\HttpClient\Factory(); 90 | // $httpclient = $factory->create($loop, $dns); 91 | $httpclient = $client->createHttpClient(); 92 | 93 | $request = $httpclient->request('GET', 'https://www.google.com/', array('user-agent'=>'none')); 94 | $request->on('response', function (Response $response) { 95 | echo '[response1]' . PHP_EOL; 96 | //var_dump($response->getHeaders()); 97 | $response->on('data', function ($data) { 98 | echo $data; 99 | }); 100 | }); 101 | $request->end(); 102 | 103 | $loop->addTimer(8, function() use ($loop) { 104 | $loop->stop(); 105 | echo 'STOP - stopping mainloop after 8 seconds' . PHP_EOL; 106 | }); 107 | 108 | $loop->run(); 109 | -------------------------------------------------------------------------------- /examples/server-middleman.php: -------------------------------------------------------------------------------- 1 | createCached('8.8.8.8', $loop); 9 | 10 | $factory = new Socks\Factory($loop, $dns); 11 | 12 | // set next SOCKS server as target 13 | $target = $factory->createClient('127.0.0.1',9050); 14 | $target->setAuth('user','p@ssw0rd'); 15 | 16 | // start a new server which forwards all connections to another SOCKS server 17 | $socket = new React\Socket\Server($loop); 18 | $server = new Socks\Server($socket, $loop, $target->createConnector()); 19 | 20 | $socket->listen('9051','localhost'); 21 | 22 | echo 'SOCKS server listening on localhost:9051 (which forwards everything to SOCKS server 127.0.0.1:9050)' . PHP_EOL; 23 | 24 | $loop->run(); 25 | -------------------------------------------------------------------------------- /examples/server.php: -------------------------------------------------------------------------------- 1 | createCached('8.8.8.8', $loop); 9 | 10 | $socket = new React\Socket\Server($loop); 11 | $socket->listen('9050','localhost'); 12 | 13 | $factory = new Socks\Factory($loop, $dns); 14 | $server = $factory->createServer($socket); 15 | $server->setAuthArray(array( 16 | 'tom' => 'god', 17 | 'user' => 'p@ssw0rd' 18 | )); 19 | 20 | echo 'SOCKS server listening on localhost:9050' . PHP_EOL; 21 | 22 | $loop->run(); 23 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Christian Lück, 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | ./Socks 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/ClientApiTest.php: -------------------------------------------------------------------------------- 1 | createCached('8.8.8.8', $loop); 11 | 12 | $factory = new Socks\Factory($loop, $dns); 13 | 14 | $this->client = $factory->createClient('127.0.0.1', 9050); 15 | } 16 | 17 | /** 18 | * @expectedException InvalidArgumentException 19 | */ 20 | public function testInvalidAuthInformation() 21 | { 22 | $this->client->setAuth(str_repeat('a', 256), 'test'); 23 | } 24 | 25 | /** 26 | * @expectedException UnexpectedValueException 27 | * @dataProvider providerInvalidAuthVersion 28 | */ 29 | public function testInvalidAuthVersion($version) 30 | { 31 | $this->client->setAuth('username', 'password'); 32 | $this->client->setProtocolVersion($version); 33 | } 34 | 35 | public function providerInvalidAuthVersion() 36 | { 37 | return array(array('4'), array('4a')); 38 | } 39 | 40 | public function testValidAuthVersion() 41 | { 42 | $this->client->setAuth('username', 'password'); 43 | $this->assertNull($this->client->setProtocolVersion(5)); 44 | } 45 | 46 | /** 47 | * @expectedException UnexpectedValueException 48 | */ 49 | public function testInvalidCanNotSetAuthenticationForSocks4() 50 | { 51 | $this->client->setProtocolVersion(4); 52 | $this->client->setAuth('username', 'password'); 53 | } 54 | 55 | public function testUnsetAuth() 56 | { 57 | // unset auth even if it's not set is valid 58 | $this->client->unsetAuth(); 59 | 60 | $this->client->setAuth('username', 'password'); 61 | $this->client->unsetAuth(); 62 | } 63 | 64 | /** 65 | * @dataProvider providerValidProtocolVersion 66 | */ 67 | public function testValidProtocolVersion($version) 68 | { 69 | $this->assertNull($this->client->setProtocolVersion($version)); 70 | } 71 | 72 | public function providerValidProtocolVersion() 73 | { 74 | return array(array('4'), array('4a'), array('5')); 75 | } 76 | 77 | /** 78 | * @expectedException InvalidArgumentException 79 | */ 80 | public function testInvalidProtocolVersion() 81 | { 82 | $this->client->setProtocolVersion(3); 83 | } 84 | 85 | public function testValidResolveLocal() 86 | { 87 | $this->assertNull($this->client->setResolveLocal(false)); 88 | $this->assertNull($this->client->setResolveLocal(true)); 89 | $this->assertNull($this->client->setProtocolVersion('4')); 90 | } 91 | 92 | /** 93 | * @expectedException UnexpectedValueException 94 | */ 95 | public function testInvalidResolveRemote() 96 | { 97 | $this->client->setProtocolVersion('4'); 98 | $this->client->setResolveLocal(false); 99 | } 100 | 101 | /** 102 | * @expectedException UnexpectedValueException 103 | */ 104 | public function testInvalidResolveRemoteVersion() 105 | { 106 | $this->client->setResolveLocal(false); 107 | $this->client->setProtocolVersion('4'); 108 | } 109 | 110 | public function testSetTimeout() 111 | { 112 | $this->client->setTimeout(1); 113 | $this->client->setTimeout(2.0); 114 | $this->client->setTimeout(3); 115 | } 116 | 117 | public function testCreateHttpClient() 118 | { 119 | $this->assertInstanceOf('\React\HttpClient\Client', $this->client->createHttpClient()); 120 | } 121 | 122 | public function testCreateConnector() 123 | { 124 | $this->assertInstanceOf('\React\SocketClient\ConnectorInterface', $this->client->createConnector()); 125 | } 126 | 127 | public function testCreateSecureConnector() 128 | { 129 | $this->assertInstanceOf('\React\SocketClient\SecureConnector', $this->client->createSecureConnector()); 130 | } 131 | 132 | /** 133 | * @dataProvider providerAddress 134 | */ 135 | public function testGetConnection($host, $port) 136 | { 137 | $this->assertInstanceOf('\React\Promise\PromiseInterface', $this->client->getConnection($host, $port)); 138 | } 139 | 140 | public function providerAddress() 141 | { 142 | return array( 143 | array('localhost','80'), 144 | array('invalid domain','non-numeric') 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /tests/FactoryTest.php: -------------------------------------------------------------------------------- 1 | factory = new Factory($this->createLoop(), $this->createResolver()); 13 | } 14 | 15 | public function testCreateClient() 16 | { 17 | $client = $this->factory->createClient('localhost', 9050); 18 | 19 | $this->assertInstanceOf('Socks\Client', $client); 20 | } 21 | 22 | public function testCreateServer() 23 | { 24 | $server = $this->factory->createServer($this->createSocket()); 25 | 26 | $this->assertInstanceOf('Socks\Server', $server); 27 | } 28 | 29 | private function createLoop() 30 | { 31 | return React\EventLoop\Factory::create(); 32 | } 33 | 34 | private function createResolver() 35 | { 36 | return $this->getMockBuilder('React\Dns\Resolver\Resolver') 37 | ->disableOriginalConstructor() 38 | ->getMock(); 39 | } 40 | 41 | private function createSocket() 42 | { 43 | return $this->getMockBuilder('React\Socket\Server') 44 | ->disableOriginalConstructor() 45 | ->getMock(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/PairTest.php: -------------------------------------------------------------------------------- 1 | loop = React\EventLoop\Factory::create(); 13 | 14 | $dnsResolverFactory = new React\Dns\Resolver\Factory(); 15 | $dns = $dnsResolverFactory->createCached('8.8.8.8', $this->loop); 16 | 17 | $this->factory = new Factory($this->loop, $dns); 18 | } 19 | 20 | public function testClientHttpRequest() 21 | { 22 | $socket = $this->createSocketServer(); 23 | $port = $socket->getPort(); 24 | $this->assertNotEquals(0, $port); 25 | 26 | $server = $this->factory->createServer($socket); 27 | 28 | $server->on('connection', function () use ($socket) { 29 | // close server socket once first connection has been established 30 | $socket->shutdown(); 31 | }); 32 | 33 | $client = $this->factory->createClient('127.0.0.1', $port); 34 | 35 | $http = $client->createHttpClient(); 36 | 37 | $request = $http->request('GET', 'https://www.google.com/', array('user-agent'=>'none')); 38 | $request->on('response', function (React\HttpClient\Response $response) { 39 | // response received, do not care for the rest of the response body 40 | $response->close(); 41 | }); 42 | $request->end(); 43 | 44 | // $loop = $this->loop; 45 | // $that = $this; 46 | // $this->loop->addTimer(1.0, function() use ($that, $loop) { 47 | // $that->fail('timeout timer'); 48 | // $loop->stop(); 49 | // }); 50 | 51 | $this->loop->run(); 52 | } 53 | 54 | private function createSocketServer() 55 | { 56 | $socket = new React\Socket\Server($this->loop); 57 | $socket->listen(0); 58 | 59 | return $socket; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/ServerApiTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('React\Socket\Server') 13 | ->disableOriginalConstructor() 14 | ->getMock(); 15 | 16 | $loop = $this->getMockBuilder('React\EventLoop\StreamSelectLoop') 17 | ->disableOriginalConstructor() 18 | ->getMock(); 19 | 20 | $connector = $this->getMockBuilder('React\SocketClient\Connector') 21 | ->disableOriginalConstructor() 22 | ->getMock(); 23 | 24 | $this->server = new Server($socket, $loop, $connector); 25 | } 26 | 27 | public function testSetProtocolVersion() 28 | { 29 | $this->server->setProtocolVersion(4); 30 | $this->server->setProtocolVersion('4a'); 31 | $this->server->setProtocolVersion(5); 32 | $this->server->setProtocolVersion(null); 33 | } 34 | 35 | /** 36 | * @expectedException InvalidArgumentException 37 | */ 38 | public function testSetInvalidProtocolVersion() 39 | { 40 | $this->server->setProtocolVersion(6); 41 | } 42 | 43 | public function testSetAuthArray() 44 | { 45 | $this->server->setAuthArray(array()); 46 | 47 | $this->server->setAuthArray(array( 48 | 'name1' => 'password1', 49 | 'name2' => 'password2' 50 | )); 51 | } 52 | 53 | /** 54 | * @expectedException InvalidArgumentException 55 | */ 56 | public function testSetAuthInvalid() 57 | { 58 | $this->server->setAuth(true); 59 | } 60 | 61 | /** 62 | * @expectedException UnexpectedValueException 63 | */ 64 | public function testUnableToSetAuthIfProtocolDoesNotSupportAuth() 65 | { 66 | $this->server->setProtocolVersion(4); 67 | 68 | $this->server->setAuthArray(array()); 69 | } 70 | 71 | /** 72 | * @expectedException UnexpectedValueException 73 | */ 74 | public function testUnableToSetProtocolWhichDoesNotSupportAuth() 75 | { 76 | $this->server->setAuthArray(array()); 77 | 78 | // this is okay 79 | $this->server->setProtocolVersion(5); 80 | 81 | $this->server->setProtocolVersion(4); 82 | } 83 | 84 | public function testUnsetAuth() 85 | { 86 | $this->server->unsetAuth(); 87 | $this->server->unsetAuth(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |