Skip to content

Commit 752d3ba

Browse files
committed
Use socket error codes for connection rejections
1 parent 21df009 commit 752d3ba

4 files changed

Lines changed: 82 additions & 21 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,12 @@ $client = new Client(
442442
);
443443
```
444444

445+
> The authentication details will be transmitted in cleartext to the SOCKS proxy
446+
server only if it requires username/password authentication.
447+
If the authentication details are missing or not accepted by the remote SOCKS
448+
proxy server, it is expected to reject each connection attempt with an
449+
exception error code of `SOCKET_EACCES` (13).
450+
445451
Authentication is only supported by protocol version 5 (SOCKS5),
446452
so passing authentication to the `Client` enforces communication with protocol
447453
version 5 and complains if you have explicitly set anything else:

src/Client.php

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,13 @@ public function handleConnectedSocks(ConnectionInterface $stream, $host, $port)
200200
}
201201
$promise->then(function () use ($deferred, $stream) {
202202
$deferred->resolve($stream);
203-
}, function($error) use ($deferred) {
204-
$deferred->reject(new Exception('Unable to communicate...', 0, $error));
203+
}, function (Exception $error) use ($deferred) {
204+
// pass custom RuntimeException through as-is, otherwise wrap in protocol error
205+
if (!$error instanceof RuntimeException) {
206+
$error = new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $error);
207+
}
208+
209+
$deferred->reject($error);
205210
});
206211

207212
return $deferred->promise()->then(
@@ -242,9 +247,12 @@ private function handleSocks4(ConnectionInterface $stream, $host, $port, StreamR
242247
'port' => 'n',
243248
'ip' => 'N'
244249
))->then(function ($data) {
245-
if ($data['null'] !== 0x00 || $data['status'] !== 0x5a) {
250+
if ($data['null'] !== 0x00) {
246251
throw new Exception('Invalid SOCKS response');
247252
}
253+
if ($data['status'] !== 0x5a) {
254+
throw new RuntimeException('Proxy refused connection with SOCKS error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
255+
}
248256
});
249257
}
250258

@@ -282,12 +290,12 @@ private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamR
282290
'status' => 'C'
283291
))->then(function ($data) {
284292
if ($data['version'] !== 0x01 || $data['status'] !== 0x00) {
285-
throw new Exception('Username/Password authentication failed');
293+
throw new RuntimeException('Username/Password authentication failed (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
286294
}
287295
});
288296
} else if ($data['method'] !== 0x00) {
289297
// any other method than "no authentication"
290-
throw new Exception('Unacceptable authentication method requested');
298+
throw new RuntimeException('No acceptable authentication method found (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13);
291299
}
292300
})->then(function () use ($stream, $reader, $host, $port) {
293301
// do not resolve hostname. only try to convert to (binary/packed) IP
@@ -312,9 +320,12 @@ private function handleSocks5(ConnectionInterface $stream, $host, $port, StreamR
312320
'type' => 'C'
313321
));
314322
})->then(function ($data) use ($reader) {
315-
if ($data['version'] !== 0x05 || $data['status'] !== 0x00 || $data['null'] !== 0x00) {
323+
if ($data['version'] !== 0x05 || $data['null'] !== 0x00) {
316324
throw new Exception('Invalid SOCKS response');
317325
}
326+
if ($data['status'] !== 0x00) {
327+
throw new RuntimeException('Proxy refused connection with SOCKS error code ' . sprintf('0x%02X', $data['status']) . ' (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111);
328+
}
318329
if ($data['type'] === 0x01) {
319330
// IPv4 address => skip IP and port
320331
return $reader->readLength(6);

tests/ClientTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ public function testEmitConnectionCloseDuringSessionWillRejectConnection()
178178
public function testEmitConnectionErrorDuringSessionWillRejectConnection()
179179
{
180180
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
181+
$stream->expects($this->once())->method('close');
181182

182183
$promise = \React\Promise\resolve($stream);
183184

@@ -189,4 +190,56 @@ public function testEmitConnectionErrorDuringSessionWillRejectConnection()
189190

190191
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EIO));
191192
}
193+
194+
public function testEmitInvalidSocks4DataDuringSessionWillRejectConnection()
195+
{
196+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
197+
$stream->expects($this->once())->method('close');
198+
199+
$promise = \React\Promise\resolve($stream);
200+
201+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
202+
203+
$promise = $this->client->connect('google.com:80');
204+
205+
$stream->emit('data', array("HTTP/1.1 400 Bad Request\r\n\r\n"));
206+
207+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
208+
}
209+
210+
public function testEmitInvalidSocks5DataDuringSessionWillRejectConnection()
211+
{
212+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
213+
$stream->expects($this->once())->method('close');
214+
215+
$promise = \React\Promise\resolve($stream);
216+
217+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
218+
219+
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
220+
221+
$promise = $this->client->connect('google.com:80');
222+
223+
$stream->emit('data', array("HTTP/1.1 400 Bad Request\r\n\r\n"));
224+
225+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
226+
}
227+
228+
public function testEmitSocks5DataErrorDuringSessionWillRejectConnection()
229+
{
230+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('write', 'close'))->getMock();
231+
$stream->expects($this->once())->method('close');
232+
233+
$promise = \React\Promise\resolve($stream);
234+
235+
$this->connector->expects($this->once())->method('connect')->with('127.0.0.1:1080?hostname=google.com')->willReturn($promise);
236+
237+
$this->client = new Client('socks5://127.0.0.1:1080', $this->connector);
238+
239+
$promise = $this->client->connect('google.com:80');
240+
241+
$stream->emit('data', array("\x05\x00" . "\x05\x01\x00\x00"));
242+
243+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
244+
}
192245
}

tests/FunctionalTest.php

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -164,29 +164,20 @@ public function testConnectionAuthenticationUnused()
164164
$this->assertResolveStream($this->client->connect('www.google.com:80'));
165165
}
166166

167-
public function testConnectionInvalidProtocolDoesNotMatchDefault()
168-
{
169-
$this->server->setProtocolVersion(5);
170-
171-
$this->client = new Client('socks4://127.0.0.1:' . $this->port, $this->connector);
172-
173-
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
174-
}
175-
176167
public function testConnectionInvalidProtocolDoesNotMatchSocks5()
177168
{
178169
$this->server->setProtocolVersion(5);
179170
$this->client = new Client('socks4a://127.0.0.1:' . $this->port, $this->connector);
180171

181-
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
172+
$this->assertRejectPromise($this->client->connect('www.google.com:80'), '', SOCKET_ECONNRESET);
182173
}
183174

184175
public function testConnectionInvalidProtocolDoesNotMatchSocks4()
185176
{
186177
$this->server->setProtocolVersion(4);
187178
$this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector);
188179

189-
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
180+
$this->assertRejectPromise($this->client->connect('www.google.com:80'), '', SOCKET_ECONNRESET);
190181
}
191182

192183
public function testConnectionInvalidNoAuthentication()
@@ -195,7 +186,7 @@ public function testConnectionInvalidNoAuthentication()
195186

196187
$this->client = new Client('socks5://127.0.0.1:' . $this->port, $this->connector);
197188

198-
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
189+
$this->assertRejectPromise($this->client->connect('www.google.com:80'), '', SOCKET_EACCES);
199190
}
200191

201192
public function testConnectionInvalidAuthenticationMismatch()
@@ -204,7 +195,7 @@ public function testConnectionInvalidAuthenticationMismatch()
204195

205196
$this->client = new Client('user:pass@127.0.0.1:' . $this->port, $this->connector);
206197

207-
$this->assertRejectPromise($this->client->connect('www.google.com:80'));
198+
$this->assertRejectPromise($this->client->connect('www.google.com:80'), '', SOCKET_EACCES);
208199
}
209200

210201
public function testConnectorOkay()
@@ -298,11 +289,11 @@ private function assertResolveStream($promise)
298289
Block\await($promise, $this->loop, 2.0);
299290
}
300291

301-
private function assertRejectPromise($promise)
292+
private function assertRejectPromise($promise, $message = '', $code = null)
302293
{
303294
$this->expectPromiseReject($promise);
304295

305-
$this->setExpectedException('Exception');
296+
$this->setExpectedException('Exception', $message, $code);
306297

307298
Block\await($promise, $this->loop, 2.0);
308299
}

0 commit comments

Comments
 (0)