├── .gitignore ├── .travis.yml ├── phpunit.xml.dist ├── examples ├── 01-server.php ├── 02-server-with-password.php ├── 11-server-proxy-chaining-with-password.php ├── 12-server-proxy-chaining-random-pool.php └── 03-server-blacklist.php ├── composer.json ├── LICENSE ├── CHANGELOG.md ├── tests ├── StreamReaderTest.php ├── bootstrap.php ├── ServerTest.php └── FunctionalTest.php ├── src ├── StreamReader.php └── Server.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7 9 | - hhvm 10 | 11 | sudo: false 12 | 13 | install: 14 | - composer install --no-interaction 15 | 16 | script: 17 | - vendor/bin/phpunit --coverage-text 18 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ./tests/ 7 | 8 | 9 | 10 | 11 | ./src/ 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/01-server.php: -------------------------------------------------------------------------------- 1 | getAddress() . PHP_EOL; 19 | 20 | $loop->run(); 21 | -------------------------------------------------------------------------------- /examples/02-server-with-password.php: -------------------------------------------------------------------------------- 1 | setAuthArray(array( 19 | 'tom' => 'god', 20 | 'user' => 'p@ssw0rd' 21 | )); 22 | 23 | echo 'SOCKS5 server requiring authentication listening on ' . $socket->getAddress() . PHP_EOL; 24 | 25 | $loop->run(); 26 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/socks-server", 3 | "description": "Async SOCKS proxy server (SOCKS4, SOCKS4a and SOCKS5), built on top of React PHP", 4 | "keywords": ["socks client", "socks server", "tcp tunnel", "socks protocol", "async", "ReactPHP"], 5 | "homepage": "https://github.com/clue/php-socks-server", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@lueck.tv" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": {"Clue\\React\\Socks\\Server\\": "src/"} 15 | }, 16 | "require": { 17 | "php": ">=5.3", 18 | "react/event-loop": "0.3.*|0.4.*", 19 | "react/socket": "^0.7", 20 | "react/stream": "^0.6 || ^0.5 || ^0.4.2", 21 | "react/promise": "^2.1 || ^1.2", 22 | "evenement/evenement": "~1.0|~2.0" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^5.0 || ^4.8", 26 | "clue/socks-react": "^0.7", 27 | "clue/connection-manager-extra": "^0.7" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 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 furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.7.0 (2017-04-14) 4 | 5 | * Feature / BC break: Update Socket to v0.7 6 | (#12 by @clue) 7 | 8 | * Improve test suite by adding PHPUnit to require-dev 9 | (#11 by @clue) 10 | 11 | > The `v0.6.0` release has been skipped intentionally to avoid confusion with 12 | the release tags of `clue/socks-react` and `react/socket`. 13 | 14 | ## 0.5.1 (2016-11-25) 15 | 16 | * Feature: Cancel pending outgoing connection if incoming connection closes 17 | (#7 by @clue) 18 | 19 | * Fix: Support empty strings for auth info (username or password) 20 | (#6 by @clue) 21 | 22 | ## 0.5.0 (2016-11-07) 23 | 24 | * First tagged release 25 | * Async SOCKS `Server` implementation 26 | * Project was originally part of [clue/socks-react](https://github.com/clue/php-socks-react) 27 | and was split off from its latest release v0.4.0 28 | (#1 by @clue) 29 | 30 | > Upgrading from clue/socks v0.4.0? Use namespace `Clue\React\Socks\Server` 31 | instead of `Clue\React\Socks` and you're ready to go! 32 | 33 | ## 0.0.0 (2011-04-26) 34 | 35 | * Initial concept, originally tracked as part of 36 | [clue/socks](https://github.com/clue/php-socks) 37 | -------------------------------------------------------------------------------- /examples/11-server-proxy-chaining-with-password.php: -------------------------------------------------------------------------------- 1 | getAddress() . ' (which forwards everything to target SOCKS server 127.0.0.1:' . $otherPort . ')' . PHP_EOL; 28 | echo 'Not already running the target SOCKS server? Try this: php 02-server-with-password.php ' . $otherPort . PHP_EOL; 29 | 30 | $loop->run(); 31 | -------------------------------------------------------------------------------- /examples/12-server-proxy-chaining-random-pool.php: -------------------------------------------------------------------------------- 1 | getAddress() . PHP_EOL; 38 | 39 | $loop->run(); 40 | -------------------------------------------------------------------------------- /examples/03-server-blacklist.php: -------------------------------------------------------------------------------- 1 | $reject, 29 | 'www.google.com:80' => $reject, 30 | '*' => $permit 31 | )); 32 | 33 | // start the server socket listening on localhost:$port for incoming socks connections 34 | $socket = new Socket($port, $loop); 35 | 36 | // start the actual socks server on the given server socket and using our connection manager for outgoing connections 37 | $server = new Server($loop, $socket, $connector); 38 | 39 | echo 'SOCKS server listening on ' . $socket->getAddress() . PHP_EOL; 40 | 41 | $loop->run(); 42 | -------------------------------------------------------------------------------- /tests/StreamReaderTest.php: -------------------------------------------------------------------------------- 1 | reader = new StreamReader(); 12 | } 13 | 14 | public function testReadByteAssertCorrect() 15 | { 16 | $this->reader->readByteAssert(0x01)->then($this->expectCallableOnce(0x01)); 17 | 18 | $this->reader->write("\x01"); 19 | } 20 | 21 | public function testReadByteAssertInvalid() 22 | { 23 | $this->reader->readByteAssert(0x02)->then(null, $this->expectCallableOnce()); 24 | 25 | $this->reader->write("\x03"); 26 | } 27 | 28 | public function testReadStringNull() 29 | { 30 | $this->reader->readStringNull()->then($this->expectCallableOnce('hello')); 31 | 32 | $this->reader->write("hello\x00"); 33 | } 34 | 35 | public function testReadStringLength() 36 | { 37 | $this->reader->readLength(5)->then($this->expectCallableOnce('hello')); 38 | 39 | $this->reader->write('he'); 40 | $this->reader->write('ll'); 41 | $this->reader->write('o '); 42 | 43 | $this->assertEquals(' ', $this->reader->getBuffer()); 44 | } 45 | 46 | public function testReadBuffered() 47 | { 48 | $this->reader->write('hello'); 49 | 50 | $this->reader->readLength(5)->then($this->expectCallableOnce('hello')); 51 | 52 | $this->assertEquals('', $this->reader->getBuffer()); 53 | } 54 | 55 | public function testSequence() 56 | { 57 | $this->reader->readByte()->then($this->expectCallableOnce(ord('h'))); 58 | $this->reader->readByteAssert(ord('e'))->then($this->expectCallableOnce(ord('e'))); 59 | $this->reader->readLength(4)->then($this->expectCallableOnce('llo ')); 60 | $this->reader->readBinary(array('w'=>'C', 'o' => 'C'))->then($this->expectCallableOnce(array('w' => ord('w'), 'o' => ord('o')))); 61 | 62 | $this->reader->write('hello world'); 63 | 64 | $this->assertEquals('rld', $this->reader->getBuffer()); 65 | } 66 | 67 | /** 68 | * @expectedException InvalidArgumentException 69 | */ 70 | public function testInvalidStructure() 71 | { 72 | $this->reader->readBinary(array('invalid' => 'y')); 73 | } 74 | 75 | /** 76 | * @expectedException InvalidArgumentException 77 | */ 78 | public function testInvalidCallback() 79 | { 80 | $this->reader->readBufferCallback(array()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | createCallableMock(); 10 | 11 | 12 | if (func_num_args() > 0) { 13 | $mock 14 | ->expects($this->once()) 15 | ->method('__invoke') 16 | ->with($this->equalTo(func_get_arg(0))); 17 | } else { 18 | $mock 19 | ->expects($this->once()) 20 | ->method('__invoke'); 21 | } 22 | 23 | return $mock; 24 | } 25 | 26 | protected function expectCallableNever() 27 | { 28 | $mock = $this->createCallableMock(); 29 | $mock 30 | ->expects($this->never()) 31 | ->method('__invoke'); 32 | 33 | return $mock; 34 | } 35 | 36 | protected function expectCallableOnceParameter($type) 37 | { 38 | $mock = $this->createCallableMock(); 39 | $mock 40 | ->expects($this->once()) 41 | ->method('__invoke') 42 | ->with($this->isInstanceOf($type)); 43 | 44 | return $mock; 45 | } 46 | 47 | /** 48 | * @link https://github.com/reactphp/react/blob/master/tests/React/Tests/Socket/TestCase.php (taken from reactphp/react) 49 | */ 50 | protected function createCallableMock() 51 | { 52 | return $this->getMockBuilder('CallableStub')->getMock(); 53 | } 54 | 55 | protected function expectPromiseResolve($promise) 56 | { 57 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 58 | 59 | $that = $this; 60 | $promise->then(null, function($error) use ($that) { 61 | $that->assertNull($error); 62 | $that->fail('promise rejected'); 63 | }); 64 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); 65 | 66 | return $promise; 67 | } 68 | 69 | protected function expectPromiseReject($promise) 70 | { 71 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 72 | 73 | $that = $this; 74 | $promise->then(function($value) use ($that) { 75 | $that->assertNull($value); 76 | $that->fail('promise resolved'); 77 | }); 78 | 79 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); 80 | 81 | return $promise; 82 | } 83 | } 84 | 85 | class CallableStub 86 | { 87 | public function __invoke() 88 | { 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/StreamReader.php: -------------------------------------------------------------------------------- 1 | buffer .= $data; 23 | 24 | do { 25 | $current = reset($this->queue); 26 | 27 | if ($current === false) { 28 | break; 29 | } 30 | 31 | /* @var $current Closure */ 32 | 33 | $ret = $current($this->buffer); 34 | 35 | if ($ret === self::RET_INCOMPLETE) { 36 | // current is incomplete, so wait for further data to arrive 37 | break; 38 | } else { 39 | // current is done, remove from list and continue with next 40 | array_shift($this->queue); 41 | } 42 | } while (true); 43 | } 44 | 45 | public function readBinary($structure) 46 | { 47 | $length = 0; 48 | $unpack = ''; 49 | foreach ($structure as $name=>$format) { 50 | if ($length !== 0) { 51 | $unpack .= '/'; 52 | } 53 | $unpack .= $format . $name; 54 | 55 | if ($format === 'C') { 56 | ++$length; 57 | } else if ($format === 'n') { 58 | $length += 2; 59 | } else if ($format === 'N') { 60 | $length += 4; 61 | } else { 62 | throw new InvalidArgumentException('Invalid format given'); 63 | } 64 | } 65 | 66 | return $this->readLength($length)->then(function ($response) use ($unpack) { 67 | return unpack($unpack, $response); 68 | }); 69 | } 70 | 71 | public function readLength($bytes) 72 | { 73 | $deferred = new Deferred(); 74 | 75 | $this->readBufferCallback(function (&$buffer) use ($bytes, $deferred) { 76 | if (strlen($buffer) >= $bytes) { 77 | $deferred->resolve((string)substr($buffer, 0, $bytes)); 78 | $buffer = (string)substr($buffer, $bytes); 79 | 80 | return StreamReader::RET_DONE; 81 | } 82 | }); 83 | 84 | return $deferred->promise(); 85 | } 86 | 87 | public function readByte() 88 | { 89 | return $this->readBinary(array( 90 | 'byte' => 'C' 91 | ))->then(function ($data) { 92 | return $data['byte']; 93 | }); 94 | } 95 | 96 | public function readByteAssert($expect) 97 | { 98 | return $this->readByte()->then(function ($byte) use ($expect) { 99 | if ($byte !== $expect) { 100 | throw new UnexpectedValueException('Unexpected byte encountered'); 101 | } 102 | return $byte; 103 | }); 104 | } 105 | 106 | public function readStringNull() 107 | { 108 | $deferred = new Deferred(); 109 | $string = ''; 110 | 111 | $that = $this; 112 | $readOne = function () use (&$readOne, $that, $deferred, &$string) { 113 | $that->readByte()->then(function ($byte) use ($deferred, &$string, $readOne) { 114 | if ($byte === 0x00) { 115 | $deferred->resolve($string); 116 | } else { 117 | $string .= chr($byte); 118 | $readOne(); 119 | } 120 | }); 121 | }; 122 | $readOne(); 123 | 124 | return $deferred->promise(); 125 | } 126 | 127 | public function readBufferCallback(/* callable */ $callable) 128 | { 129 | if (!is_callable($callable)) { 130 | throw new InvalidArgumentException('Given function must be callable'); 131 | } 132 | 133 | if ($this->queue) { 134 | $this->queue []= $callable; 135 | } else { 136 | $this->queue = array($callable); 137 | 138 | if ($this->buffer !== '') { 139 | // this is the first element in the queue and the buffer is filled => trigger write procedure 140 | $this->write(''); 141 | } 142 | } 143 | } 144 | 145 | public function getBuffer() 146 | { 147 | return $this->buffer; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/ServerTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('React\Socket\ServerInterface') 16 | ->getMock(); 17 | 18 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface') 19 | ->getMock(); 20 | 21 | $this->connector = $this->getMockBuilder('React\Socket\ConnectorInterface') 22 | ->getMock(); 23 | 24 | $this->server = new Server($loop, $socket, $this->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 testConnectWillCreateConnection() 85 | { 86 | $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); 87 | 88 | $promise = new Promise(function () { }); 89 | 90 | $this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise); 91 | 92 | $promise = $this->server->connectTarget($stream, array('google.com', 80)); 93 | 94 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); 95 | } 96 | 97 | public function testConnectWillRejectIfConnectionFails() 98 | { 99 | $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); 100 | 101 | $promise = new Promise(function ($_, $reject) { $reject(new \RuntimeException()); }); 102 | 103 | $this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise); 104 | 105 | $promise = $this->server->connectTarget($stream, array('google.com', 80)); 106 | 107 | $promise->then(null, $this->expectCallableOnce()); 108 | } 109 | 110 | public function testConnectWillCancelConnectionIfStreamCloses() 111 | { 112 | $stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close'))->getMock(); 113 | 114 | $promise = new Promise(function () { }, function () { 115 | throw new \RuntimeException(); 116 | }); 117 | 118 | 119 | $this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise); 120 | 121 | $promise = $this->server->connectTarget($stream, array('google.com', 80)); 122 | 123 | $stream->emit('close'); 124 | 125 | $promise->then(null, $this->expectCallableOnce()); 126 | } 127 | 128 | public function testConnectWillAbortIfPromiseIsCancelled() 129 | { 130 | $stream = $this->getMockBuilder('React\Socket\ConnectionInterface')->getMock(); 131 | 132 | $promise = new Promise(function () { }, function () { 133 | throw new \RuntimeException(); 134 | }); 135 | 136 | $this->connector->expects($this->once())->method('connect')->with('google.com:80')->willReturn($promise); 137 | 138 | $promise = $this->server->connectTarget($stream, array('google.com', 80)); 139 | 140 | $promise->cancel(); 141 | 142 | $promise->then(null, $this->expectCallableOnce()); 143 | } 144 | 145 | public function testHandleSocksConnectionWillEndOnInvalidData() 146 | { 147 | $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock(); 148 | $connection->expects($this->once())->method('pause'); 149 | $connection->expects($this->once())->method('end'); 150 | 151 | $this->server->onConnection($connection); 152 | 153 | $connection->emit('data', array('asdasdasdasdasd')); 154 | } 155 | 156 | public function testHandleSocksConnectionWillEstablishOutgoingConnection() 157 | { 158 | $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock(); 159 | 160 | $promise = new Promise(function () { }); 161 | 162 | $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise); 163 | 164 | $this->server->onConnection($connection); 165 | 166 | $connection->emit('data', array("\x04\x01" . "\x00\x50" . pack('N', ip2long('127.0.0.1')) . "\x00")); 167 | } 168 | 169 | public function testHandleSocksConnectionWillCancelOutputConnectionIfIncomingCloses() 170 | { 171 | $connection = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('pause', 'end'))->getMock(); 172 | 173 | $promise = new Promise(function () { }, $this->expectCallableOnce()); 174 | 175 | $this->connector->expects($this->once())->method('connect')->with('127.0.0.1:80')->willReturn($promise); 176 | 177 | $this->server->onConnection($connection); 178 | 179 | $connection->emit('data', array("\x04\x01" . "\x00\x50" . pack('N', ip2long('127.0.0.1')) . "\x00")); 180 | $connection->emit('close'); 181 | } 182 | 183 | public function testUnsetAuth() 184 | { 185 | $this->server->unsetAuth(); 186 | $this->server->unsetAuth(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /tests/FunctionalTest.php: -------------------------------------------------------------------------------- 1 | loop = React\EventLoop\Factory::create(); 22 | 23 | $socket = new React\Socket\Server(0, $this->loop); 24 | $this->port = parse_url('tcp://' . $socket->getAddress(), PHP_URL_PORT); 25 | $this->assertNotEquals(0, $this->port); 26 | 27 | $this->server = new Server($this->loop, $socket); 28 | $this->connector = new TcpConnector($this->loop); 29 | $this->client = new Client('127.0.0.1:' . $this->port, $this->connector); 30 | } 31 | 32 | public function testConnection() 33 | { 34 | $this->assertResolveStream($this->client->connect('www.google.com:80')); 35 | } 36 | 37 | public function testConnectionInvalid() 38 | { 39 | $this->assertRejectPromise($this->client->connect('www.google.com.invalid:80')); 40 | } 41 | 42 | public function testConnectionSocks4() 43 | { 44 | $this->server->setProtocolVersion('4'); 45 | $this->client = new Client('socks4://127.0.0.1:' . $this->port, $this->connector); 46 | 47 | $this->assertResolveStream($this->client->connect('127.0.0.1:' . $this->port)); 48 | } 49 | 50 | public function testConnectionSocks4a() 51 | { 52 | $this->server->setProtocolVersion('4a'); 53 | $this->client = new Client('socks4a://127.0.0.1:' . $this->port, $this->connector); 54 | 55 | $this->assertResolveStream($this->client->connect('www.google.com:80')); 56 | } 57 | 58 | public function testConnectionSocks5() 59 | { 60 | $this->server->setProtocolVersion(5); 61 | $this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector); 62 | 63 | $this->assertResolveStream($this->client->connect('www.google.com:80')); 64 | } 65 | 66 | public function testConnectionAuthentication() 67 | { 68 | $this->server->setAuthArray(array('name' => 'pass')); 69 | $this->client = new Client('name:pass@127.0.0.1:' . $this->port, $this->connector); 70 | 71 | $this->assertResolveStream($this->client->connect('www.google.com:80')); 72 | } 73 | 74 | public function testConnectionAuthenticationEmptyPassword() 75 | { 76 | $this->server->setAuthArray(array('user' => '')); 77 | $this->client = new Client('user@127.0.0.1:' . $this->port, $this->connector); 78 | 79 | $this->assertResolveStream($this->client->connect('www.google.com:80')); 80 | } 81 | 82 | public function testConnectionAuthenticationUnused() 83 | { 84 | $this->client = new Client('name:pass@127.0.0.1:' . $this->port, $this->connector); 85 | 86 | $this->assertResolveStream($this->client->connect('www.google.com:80')); 87 | } 88 | 89 | public function testConnectionInvalidProtocolDoesNotMatchDefault() 90 | { 91 | $this->server->setProtocolVersion(5); 92 | 93 | $this->assertRejectPromise($this->client->connect('www.google.com:80')); 94 | } 95 | 96 | public function testConnectionInvalidProtocolDoesNotMatchSocks5() 97 | { 98 | $this->server->setProtocolVersion(5); 99 | $this->client = new Client('socks4a://127.0.0.1:' . $this->port, $this->connector); 100 | 101 | $this->assertRejectPromise($this->client->connect('www.google.com:80')); 102 | } 103 | 104 | public function testConnectionInvalidProtocolDoesNotMatchSocks4() 105 | { 106 | $this->server->setProtocolVersion(4); 107 | $this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector); 108 | 109 | $this->assertRejectPromise($this->client->connect('www.google.com:80')); 110 | } 111 | 112 | public function testConnectionInvalidNoAuthentication() 113 | { 114 | $this->server->setAuthArray(array('name' => 'pass')); 115 | $this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector); 116 | 117 | $this->assertRejectPromise($this->client->connect('www.google.com:80')); 118 | } 119 | 120 | public function testConnectionInvalidAuthenticationMismatch() 121 | { 122 | $this->server->setAuthArray(array('name' => 'pass')); 123 | $this->client = new Client('user:pass@127.0.0.1:' . $this->port, $this->connector); 124 | 125 | $this->assertRejectPromise($this->client->connect('www.google.com:80')); 126 | } 127 | 128 | public function testConnectorInvalidUnboundPortTimeout() 129 | { 130 | $tcp = new TimeoutConnector($this->client, 0.1, $this->loop); 131 | 132 | $this->assertRejectPromise($tcp->connect('www.google.com:8080')); 133 | } 134 | 135 | public function testSecureConnectorOkay() 136 | { 137 | if (!function_exists('stream_socket_enable_crypto')) { 138 | $this->markTestSkipped('Required function does not exist in your environment (HHVM?)'); 139 | } 140 | 141 | $ssl = new SecureConnector($this->client, $this->loop); 142 | 143 | $this->assertResolveStream($ssl->connect('www.google.com:443')); 144 | } 145 | 146 | public function testSecureConnectorToBadSslWithVerifyFails() 147 | { 148 | if (!function_exists('stream_socket_enable_crypto')) { 149 | $this->markTestSkipped('Required function does not exist in your environment (HHVM?)'); 150 | } 151 | 152 | $ssl = new SecureConnector($this->client, $this->loop, array('verify_peer' => true)); 153 | 154 | $this->assertRejectPromise($ssl->connect('self-signed.badssl.com:443')); 155 | } 156 | 157 | public function testSecureConnectorToBadSslWithoutVerifyWorks() 158 | { 159 | if (!function_exists('stream_socket_enable_crypto')) { 160 | $this->markTestSkipped('Required function does not exist in your environment (HHVM?)'); 161 | } 162 | 163 | $ssl = new SecureConnector($this->client, $this->loop, array('verify_peer' => false)); 164 | 165 | $this->assertResolveStream($ssl->connect('self-signed.badssl.com:443')); 166 | } 167 | 168 | public function testSecureConnectorInvalidPlaintextIsNotSsl() 169 | { 170 | if (!function_exists('stream_socket_enable_crypto')) { 171 | $this->markTestSkipped('Required function does not exist in your environment (HHVM?)'); 172 | } 173 | 174 | $ssl = new SecureConnector($this->client, $this->loop); 175 | 176 | $this->assertRejectPromise($ssl->connect('www.google.com:80')); 177 | } 178 | 179 | public function testSecureConnectorInvalidUnboundPortTimeout() 180 | { 181 | $tcp = new TimeoutConnector($this->client, 0.1, $this->loop); 182 | $ssl = new SecureConnector($tcp, $this->loop); 183 | 184 | $this->assertRejectPromise($ssl->connect('www.google.com:8080')); 185 | } 186 | 187 | private function assertResolveStream($promise) 188 | { 189 | $this->expectPromiseResolve($promise); 190 | 191 | $promise->then(function ($stream) { 192 | $stream->close(); 193 | }); 194 | 195 | $this->waitFor($promise); 196 | } 197 | 198 | private function assertRejectPromise($promise) 199 | { 200 | $this->expectPromiseReject($promise); 201 | 202 | $this->setExpectedException('Exception'); 203 | $this->waitFor($promise); 204 | } 205 | 206 | private function waitFor(PromiseInterface $promise) 207 | { 208 | $resolved = null; 209 | $exception = null; 210 | 211 | $promise->then(function ($c) use (&$resolved) { 212 | $resolved = $c; 213 | }, function($error) use (&$exception) { 214 | $exception = $error; 215 | }); 216 | 217 | while ($resolved === null && $exception === null) { 218 | $this->loop->tick(); 219 | } 220 | 221 | if ($exception !== null) { 222 | throw $exception; 223 | } 224 | 225 | return $resolved; 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | This package has now been merged into 4 | [clue/socks-react](https://github.com/clue/php-socks-react) and only exists for BC 5 | reasons. 6 | 7 | ```bash 8 | $ composer require clue/socks-react 9 | ``` 10 | 11 | If you've previously used this package to build a SOCKS server, 12 | upgrading should take no longer than a few minutes. 13 | All classes have been merged as-is from the latest `v0.7.0` release with no 14 | other changes, so you can simply update your code to use the updated namespace 15 | like this: 16 | 17 | ```php 18 | // old from clue/socks-server 19 | $server = new Clue\React\Socks\Server\Server($loop, $socket); 20 | 21 | // new 22 | $server = new Clue\React\Socks\Server($loop, $socket); 23 | ``` 24 | 25 | See https://github.com/clue/php-socks-react for more details. 26 | 27 | The below documentation applies to the last release of this package. 28 | Further development will take place in the updated 29 | [clue/socks-react](https://github.com/clue/php-socks-react), so you're highly 30 | recommended to upgrade as soon as possible. 31 | 32 | # Legacy clue/socks-server [![Build Status](https://travis-ci.org/clue/php-socks-server.svg?branch=master)](https://travis-ci.org/clue/php-socks-server) 33 | 34 | Async SOCKS proxy server (SOCKS4, SOCKS4a and SOCKS5), built on top of React PHP. 35 | 36 | The SOCKS protocol family can be used to easily tunnel TCP connections independent 37 | of the actual application level protocol, such as HTTP, SMTP, IMAP, Telnet etc. 38 | 39 | **Table of contents** 40 | 41 | * [Quickstart example](#quickstart-example) 42 | * [Usage](#usage) 43 | * [Server](#server) 44 | * [Protocol version](#protocol-version) 45 | * [Authentication](#authentication) 46 | * [Proxy chaining](#proxy-chaining) 47 | * [Install](#install) 48 | * [Tests](#tests) 49 | * [License](#license) 50 | * [More](#more) 51 | 52 | ## Quickstart example 53 | 54 | Once [installed](#install), you can use the following code to create a SOCKS 55 | proxy server listening for connections on `localhost:1080`: 56 | 57 | ```php 58 | $loop = React\EventLoop\Factory::create(); 59 | 60 | // listen on localhost:1080 61 | $socket = new Socket($loop); 62 | $socket->listen(1080,'localhost'); 63 | 64 | // start a new server listening for incoming connection on the given socket 65 | $server = new Server($loop, $socket); 66 | 67 | $loop->run(); 68 | ``` 69 | 70 | See also the [examples](examples). 71 | 72 | ## Usage 73 | 74 | ### Server 75 | 76 | The `Server` is responsible for accepting incoming communication from SOCKS clients 77 | and forwarding the requested connection to the target host. 78 | It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) 79 | and an underlying TCP/IP socket server like this: 80 | 81 | ```php 82 | $loop = \React\EventLoop\Factory::create(); 83 | 84 | // listen on localhost:$port 85 | $socket = new Socket($loop); 86 | $socket->listen($port,'localhost'); 87 | 88 | $server = new Server($loop, $socket); 89 | ``` 90 | 91 | If you need custom connector settings (DNS resolution, timeouts etc.), you can explicitly pass a 92 | custom instance of the [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): 93 | 94 | ```php 95 | // use local DNS server 96 | $dnsResolverFactory = new DnsFactory(); 97 | $resolver = $dnsResolverFactory->createCached('127.0.0.1', $loop); 98 | 99 | // outgoing connections to target host via interface 192.168.10.1 100 | $connector = new DnsConnector( 101 | new TcpConnector($loop, array('bindto' => '192.168.10.1:0')), 102 | $resolver 103 | ); 104 | 105 | $server = new Server($loop, $socket, $connector); 106 | ``` 107 | 108 | #### Protocol version 109 | 110 | The `Server` supports all protocol versions (SOCKS4, SOCKS4a and SOCKS5) by default. 111 | 112 | While SOCKS4 already had (a somewhat limited) support for `SOCKS BIND` requests 113 | and SOCKS5 added generic UDP support (`SOCKS UDPASSOCIATE`), this library 114 | focuses on the most commonly used core feature of `SOCKS CONNECT`. 115 | In this mode, a SOCKS server acts as a generic proxy allowing higher level 116 | application protocols to work through it. 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 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
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
168 | 169 | Note, this is __not__ a full SOCKS5 implementation due to missing GSSAPI 170 | authentication (but it's unlikely you're going to miss it anyway). 171 | 172 | If want to explicitly set the protocol version, use the supported values `4`, `4a` or `5`: 173 | 174 | ```PHP 175 | $server->setProtocolVersion(5); 176 | ``` 177 | 178 | In order to reset the protocol version to its default (i.e. automatic detection), 179 | use `null` as protocol version. 180 | 181 | ```PHP 182 | $server->setProtocolVersion(null); 183 | ``` 184 | 185 | #### Authentication 186 | 187 | By default, the `Server` does not require any authentication from the clients. 188 | You can enable authentication support so that clients need to pass a valid 189 | username and password before forwarding any connections. 190 | 191 | Setting authentication on the `Server` enforces each further connected client 192 | to use protocol version 5 (SOCKS5). 193 | If a client tries to use any other protocol version, does not send along 194 | authentication details or if authentication details can not be verified, 195 | the connection will be rejected. 196 | 197 | Because your authentication mechanism might take some time to actually check 198 | the provided authentication credentials (like querying a remote database or webservice), 199 | the server side uses a [Promise](https://github.com/reactphp/promise) based interface. 200 | While this might seem complex at first, it actually provides a very simple way 201 | to handle simultanous connections in a non-blocking fashion and increases overall performance. 202 | 203 | ```PHP 204 | $server->setAuth(function ($username, $password) { 205 | // either return a boolean success value right away 206 | // or use promises for delayed authentication 207 | }); 208 | ``` 209 | 210 | Or if you only accept static authentication details, you can use the simple 211 | array-based authentication method as a shortcut: 212 | 213 | ```PHP 214 | $server->setAuthArray(array( 215 | 'tom' => 'password', 216 | 'admin' => 'root' 217 | )); 218 | ``` 219 | 220 | See also the [second example](examples). 221 | 222 | If you do not want to use authentication anymore: 223 | 224 | ```PHP 225 | $server->unsetAuth(); 226 | ``` 227 | 228 | #### Proxy chaining 229 | 230 | The `Server` is responsible for creating connections to the target host. 231 | 232 | ``` 233 | Client -> SocksServer -> TargetHost 234 | ``` 235 | 236 | Sometimes it may be required to establish outgoing connections via another SOCKS 237 | server. 238 | For example, this can be useful if your target SOCKS server requires 239 | authentication, but your client does not support sending authentication 240 | information (e.g. like most webbrowser). 241 | 242 | ``` 243 | Client -> MiddlemanSocksServer -> TargetSocksServer -> TargetHost 244 | ``` 245 | 246 | The `Server` uses any instance of the `ConnectorInterface` to establish outgoing 247 | connections. 248 | In order to connect through another SOCKS server, you can simply use a SOCKS 249 | connector from the following SOCKS client package: 250 | 251 | ```bash 252 | $ composer require clue/socks-react:^0.7 253 | ``` 254 | 255 | You can now create a SOCKS `Client` instance like this: 256 | 257 | ```php 258 | // set next SOCKS server localhost:$targetPort as target 259 | $connector = new React\Socket\TcpConnector($loop); 260 | $client = new Clue\React\Socks\Client('user:pass@127.0.0.1:' . $targetPort, $connector); 261 | 262 | // listen on localhost:$middlemanPort 263 | $socket = new Socket($loop); 264 | $socket->listen($middlemanPort, 'localhost'); 265 | 266 | // start a new server which forwards all connections to the other SOCKS server 267 | $server = new Server($loop, $socket, $client); 268 | ``` 269 | 270 | See also the [example #11](examples). 271 | 272 | Proxy chaining can happen on the server side and/or the client side: 273 | 274 | * If you ask your client to chain through multiple proxies, then each proxy 275 | server does not really know anything about chaining at all. 276 | This means that this is a client-only property and not part of this project. 277 | For example, you can find this in the companion SOCKS client side project 278 | [clue/socks-react](https://github.com/clue/php-socks-react#proxy-chaining). 279 | 280 | * If you ask your server to chain through another proxy, then your client does 281 | not really know anything about chaining at all. 282 | This means that this is a server-only property and can be implemented as above. 283 | 284 | ## Install 285 | 286 | The recommended way to install this library is [through Composer](http://getcomposer.org). 287 | [New to Composer?](http://getcomposer.org/doc/00-intro.md) 288 | 289 | This will install the latest supported version: 290 | 291 | ```bash 292 | $ composer require clue/socks-server:^0.7 293 | ``` 294 | 295 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 296 | 297 | ## Tests 298 | 299 | To run the test suite, you first need to clone this repo and then install all 300 | dependencies [through Composer](http://getcomposer.org): 301 | 302 | ```bash 303 | $ composer install 304 | ``` 305 | 306 | To run the test suite, go to the project root and run: 307 | 308 | ```bash 309 | $ php vendor/bin/phpunit 310 | ``` 311 | 312 | ## License 313 | 314 | MIT, see LICENSE 315 | 316 | ## More 317 | 318 | * If you're looking for an end-user SOCKS server daemon, you may want to 319 | use [clue/psocksd](https://github.com/clue/psocksd). 320 | * If you're looking for a SOCKS client implementation, consider using 321 | [clue/socks-react](https://github.com/clue/php-socks-react). 322 | -------------------------------------------------------------------------------- /src/Server.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 37 | $this->connector = $connector; 38 | 39 | $that = $this; 40 | $serverInterface->on('connection', function ($connection) use ($that) { 41 | $that->emit('connection', array($connection)); 42 | $that->onConnection($connection); 43 | }); 44 | } 45 | 46 | public function setProtocolVersion($version) 47 | { 48 | if ($version !== null) { 49 | $version = (string)$version; 50 | if (!in_array($version, array('4', '4a', '5'), true)) { 51 | throw new InvalidArgumentException('Invalid protocol version given'); 52 | } 53 | if ($version !== '5' && $this->auth !== null){ 54 | throw new UnexpectedValueException('Unable to change protocol version to anything but SOCKS5 while authentication is used. Consider removing authentication info or sticking to SOCKS5'); 55 | } 56 | } 57 | $this->protocolVersion = $version; 58 | } 59 | 60 | public function setAuth($auth) 61 | { 62 | if (!is_callable($auth)) { 63 | throw new InvalidArgumentException('Given authenticator is not a valid callable'); 64 | } 65 | if ($this->protocolVersion !== null && $this->protocolVersion !== '5') { 66 | throw new UnexpectedValueException('Authentication requires SOCKS5. Consider using protocol version 5 or waive authentication'); 67 | } 68 | // wrap authentication callback in order to cast its return value to a promise 69 | $this->auth = function($username, $password) use ($auth) { 70 | $ret = call_user_func($auth, $username, $password); 71 | if ($ret instanceof PromiseInterface) { 72 | return $ret; 73 | } 74 | $deferred = new Deferred(); 75 | $ret ? $deferred->resolve() : $deferred->reject(); 76 | return $deferred->promise(); 77 | }; 78 | } 79 | 80 | public function setAuthArray(array $login) 81 | { 82 | $this->setAuth(function ($username, $password) use ($login) { 83 | return (isset($login[$username]) && (string)$login[$username] === $password); 84 | }); 85 | } 86 | 87 | public function unsetAuth() 88 | { 89 | $this->auth = null; 90 | } 91 | 92 | public function onConnection(ConnectionInterface $connection) 93 | { 94 | $that = $this; 95 | $handling = $this->handleSocks($connection)->then(function($remote) use ($connection){ 96 | $connection->emit('ready',array($remote)); 97 | }, function ($error) use ($connection, $that) { 98 | if (!($error instanceof \Exception)) { 99 | $error = new \Exception($error); 100 | } 101 | $connection->emit('error', array($error)); 102 | $that->endConnection($connection); 103 | }); 104 | 105 | $connection->on('close', function () use ($handling) { 106 | $handling->cancel(); 107 | }); 108 | } 109 | 110 | /** 111 | * gracefully shutdown connection by flushing all remaining data and closing stream 112 | */ 113 | public function endConnection(ConnectionInterface $stream) 114 | { 115 | $tid = true; 116 | $loop = $this->loop; 117 | 118 | // cancel below timer in case connection is closed in time 119 | $stream->once('close', function () use (&$tid, $loop) { 120 | // close event called before the timer was set up, so everything is okay 121 | if ($tid === true) { 122 | // make sure to not start a useless timer 123 | $tid = false; 124 | } else { 125 | $loop->cancelTimer($tid); 126 | } 127 | }); 128 | 129 | // shut down connection by pausing input data, flushing outgoing buffer and then exit 130 | $stream->pause(); 131 | $stream->end(); 132 | 133 | // check if connection is not already closed 134 | if ($tid === true) { 135 | // fall back to forcefully close connection in 3 seconds if buffer can not be flushed 136 | $tid = $loop->addTimer(3.0, array($stream,'close')); 137 | } 138 | } 139 | 140 | private function handleSocks(ConnectionInterface $stream) 141 | { 142 | $reader = new StreamReader(); 143 | $stream->on('data', array($reader, 'write')); 144 | 145 | $that = $this; 146 | $that = $this; 147 | 148 | $auth = $this->auth; 149 | $protocolVersion = $this->protocolVersion; 150 | 151 | // authentication requires SOCKS5 152 | if ($auth !== null) { 153 | $protocolVersion = '5'; 154 | } 155 | 156 | return $reader->readByte()->then(function ($version) use ($stream, $that, $protocolVersion, $auth, $reader){ 157 | if ($version === 0x04) { 158 | if ($protocolVersion === '5') { 159 | throw new UnexpectedValueException('SOCKS4 not allowed due to configuration'); 160 | } 161 | return $that->handleSocks4($stream, $protocolVersion, $reader); 162 | } else if ($version === 0x05) { 163 | if ($protocolVersion !== null && $protocolVersion !== '5') { 164 | throw new UnexpectedValueException('SOCKS5 not allowed due to configuration'); 165 | } 166 | return $that->handleSocks5($stream, $auth, $reader); 167 | } 168 | throw new UnexpectedValueException('Unexpected/unknown version number'); 169 | }); 170 | } 171 | 172 | public function handleSocks4(ConnectionInterface $stream, $protocolVersion, StreamReader $reader) 173 | { 174 | // suppliying hostnames is only allowed for SOCKS4a (or automatically detected version) 175 | $supportsHostname = ($protocolVersion === null || $protocolVersion === '4a'); 176 | 177 | $that = $this; 178 | return $reader->readByteAssert(0x01)->then(function () use ($reader) { 179 | return $reader->readBinary(array( 180 | 'port' => 'n', 181 | 'ipLong' => 'N', 182 | 'null' => 'C' 183 | )); 184 | })->then(function ($data) use ($reader, $supportsHostname) { 185 | if ($data['null'] !== 0x00) { 186 | throw new Exception('Not a null byte'); 187 | } 188 | if ($data['ipLong'] === 0) { 189 | throw new Exception('Invalid IP'); 190 | } 191 | if ($data['port'] === 0) { 192 | throw new Exception('Invalid port'); 193 | } 194 | if ($data['ipLong'] < 256 && $supportsHostname) { 195 | // invalid IP => probably a SOCKS4a request which appends the hostname 196 | return $reader->readStringNull()->then(function ($string) use ($data){ 197 | return array($string, $data['port']); 198 | }); 199 | } else { 200 | $ip = long2ip($data['ipLong']); 201 | return array($ip, $data['port']); 202 | } 203 | })->then(function ($target) use ($stream, $that) { 204 | return $that->connectTarget($stream, $target)->then(function (ConnectionInterface $remote) use ($stream){ 205 | $stream->write(pack('C8', 0x00, 0x5a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); 206 | 207 | return $remote; 208 | }, function($error) use ($stream){ 209 | $stream->end(pack('C8', 0x00, 0x5b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)); 210 | 211 | throw $error; 212 | }); 213 | }, function($error) { 214 | throw new UnexpectedValueException('SOCKS4 protocol error',0,$error); 215 | }); 216 | } 217 | 218 | public function handleSocks5(ConnectionInterface $stream, $auth=null, StreamReader $reader) 219 | { 220 | $that = $this; 221 | return $reader->readByte()->then(function ($num) use ($reader) { 222 | // $num different authentication mechanisms offered 223 | return $reader->readLength($num); 224 | })->then(function ($methods) use ($reader, $stream, $auth) { 225 | if ($auth === null && strpos($methods,"\x00") !== false) { 226 | // accept "no authentication" 227 | $stream->write(pack('C2', 0x05, 0x00)); 228 | return 0x00; 229 | } else if ($auth !== null && strpos($methods,"\x02") !== false) { 230 | // username/password authentication (RFC 1929) sub negotiation 231 | $stream->write(pack('C2', 0x05, 0x02)); 232 | return $reader->readByteAssert(0x01)->then(function () use ($reader) { 233 | return $reader->readByte(); 234 | })->then(function ($length) use ($reader) { 235 | return $reader->readLength($length); 236 | })->then(function ($username) use ($reader, $auth, $stream) { 237 | return $reader->readByte()->then(function ($length) use ($reader) { 238 | return $reader->readLength($length); 239 | })->then(function ($password) use ($username, $auth, $stream) { 240 | // username and password known => authenticate 241 | // echo 'auth: ' . $username.' : ' . $password . PHP_EOL; 242 | return $auth($username, $password)->then(function () use ($stream, $username) { 243 | // accept 244 | $stream->emit('auth', array($username)); 245 | $stream->write(pack('C2', 0x01, 0x00)); 246 | }, function() use ($stream) { 247 | // reject => send any code but 0x00 248 | $stream->end(pack('C2', 0x01, 0xFF)); 249 | throw new UnexpectedValueException('Unable to authenticate'); 250 | }); 251 | }); 252 | }); 253 | } else { 254 | // reject all offered authentication methods 255 | $stream->end(pack('C2', 0x05, 0xFF)); 256 | throw new UnexpectedValueException('No acceptable authentication mechanism found'); 257 | } 258 | })->then(function ($method) use ($reader, $stream) { 259 | return $reader->readBinary(array( 260 | 'version' => 'C', 261 | 'command' => 'C', 262 | 'null' => 'C', 263 | 'type' => 'C' 264 | )); 265 | })->then(function ($data) use ($reader) { 266 | if ($data['version'] !== 0x05) { 267 | throw new UnexpectedValueException('Invalid SOCKS version'); 268 | } 269 | if ($data['command'] !== 0x01) { 270 | throw new UnexpectedValueException('Only CONNECT requests supported'); 271 | } 272 | // if ($data['null'] !== 0x00) { 273 | // throw new UnexpectedValueException('Reserved byte has to be NULL'); 274 | // } 275 | if ($data['type'] === 0x03) { 276 | // target hostname string 277 | return $reader->readByte()->then(function ($len) use ($reader) { 278 | return $reader->readLength($len); 279 | }); 280 | } else if ($data['type'] === 0x01) { 281 | // target IPv4 282 | return $reader->readLength(4)->then(function ($addr) { 283 | return inet_ntop($addr); 284 | }); 285 | } else if ($data['type'] === 0x04) { 286 | // target IPv6 287 | return $reader->readLength(16)->then(function ($addr) { 288 | return inet_ntop($addr); 289 | }); 290 | } else { 291 | throw new UnexpectedValueException('Invalid target type'); 292 | } 293 | })->then(function ($host) use ($reader) { 294 | return $reader->readBinary(array('port'=>'n'))->then(function ($data) use ($host) { 295 | return array($host, $data['port']); 296 | }); 297 | })->then(function ($target) use ($that, $stream) { 298 | return $that->connectTarget($stream, $target); 299 | }, function($error) use ($stream) { 300 | throw new UnexpectedValueException('SOCKS5 protocol error',0,$error); 301 | })->then(function (ConnectionInterface $remote) use ($stream) { 302 | $stream->write(pack('C4Nn', 0x05, 0x00, 0x00, 0x01, 0, 0)); 303 | 304 | return $remote; 305 | }, function(Exception $error) use ($stream){ 306 | $code = 0x01; 307 | $stream->end(pack('C4Nn', 0x05, $code, 0x00, 0x01, 0, 0)); 308 | 309 | throw $error; 310 | }); 311 | } 312 | 313 | public function connectTarget(ConnectionInterface $stream, array $target) 314 | { 315 | $stream->emit('target', $target); 316 | $that = $this; 317 | $connecting = $this->connector->connect($target[0] . ':' . $target[1]); 318 | 319 | $stream->on('close', function () use ($connecting) { 320 | $connecting->cancel(); 321 | }); 322 | 323 | return $connecting->then(function (ConnectionInterface $remote) use ($stream, $that) { 324 | $stream->pipe($remote, array('end'=>false)); 325 | $remote->pipe($stream, array('end'=>false)); 326 | 327 | // remote end closes connection => stop reading from local end, try to flush buffer to local and disconnect local 328 | $remote->on('end', function() use ($stream, $that) { 329 | $stream->emit('shutdown', array('remote', null)); 330 | $that->endConnection($stream); 331 | }); 332 | 333 | // local end closes connection => stop reading from remote end, try to flush buffer to remote and disconnect remote 334 | $stream->on('end', function() use ($remote, $that) { 335 | $that->endConnection($remote); 336 | }); 337 | 338 | // set bigger buffer size of 100k to improve performance 339 | $stream->bufferSize = $remote->bufferSize = 100 * 1024 * 1024; 340 | 341 | return $remote; 342 | }, function(Exception $error) { 343 | throw new UnexpectedValueException('Unable to connect to remote target', 0, $error); 344 | }); 345 | } 346 | } 347 | --------------------------------------------------------------------------------