├── .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 [](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 | SOCKS4 |
122 | SOCKS4a |
123 | SOCKS5 |
124 |
125 |
126 | | Protocol specification |
127 | SOCKS4.protocol |
128 | SOCKS4A.protocol |
129 | RFC 1928 |
130 |
131 |
132 | | Tunnel outgoing TCP connections |
133 | ✓ |
134 | ✓ |
135 | ✓ |
136 |
137 |
138 | | Remote DNS resolving |
139 | ✗ |
140 | ✓ |
141 | ✓ |
142 |
143 |
144 | | IPv6 addresses |
145 | ✗ |
146 | ✗ |
147 | ✓ |
148 |
149 |
150 | | Username/Password authentication |
151 | ✗ |
152 | ✗ |
153 | ✓ (as per RFC 1929) |
154 |
155 |
156 | | Handshake # roundtrips |
157 | 1 |
158 | 1 |
159 | 2 (3 with authentication) |
160 |
161 |
162 | Handshake traffic + remote DNS |
163 | 17 bytes ✗ |
164 | 17 bytes + hostname + 1 |
165 | variable (+ auth + IPv6) + hostname - 3 |
166 |
167 |
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 |
--------------------------------------------------------------------------------