Skip to content

Commit 1b23773

Browse files
authored
Merge pull request #17 from clue-labs/errors
Use socket error codes for connection rejections
2 parents 104d568 + 7258a76 commit 1b23773

File tree

5 files changed

+77
-18
lines changed

5 files changed

+77
-18
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,8 @@ $proxy = new ProxyConnector(
295295
connection attempt.
296296
If the authentication details are missing or not accepted by the remote HTTP
297297
proxy server, it is expected to reject each connection attempt with a
298-
`407` (Proxy Authentication Required) response status code.
298+
`407` (Proxy Authentication Required) response status code and an exception
299+
error code of `SOCKET_EACCES` (13).
299300

300301
#### Advanced secure proxy connections
301302

src/ProxyConnector.php

+15-10
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public function connect($uri)
128128

129129
return $this->connector->connect($proxyUri)->then(function (ConnectionInterface $stream) use ($host, $port, $auth) {
130130
$deferred = new Deferred(function ($_, $reject) use ($stream) {
131-
$reject(new RuntimeException('Operation canceled while waiting for response from proxy'));
131+
$reject(new RuntimeException('Connection canceled while waiting for response from proxy (ECONNABORTED)', defined('SOCKET_ECONNABORTED') ? SOCKET_ECONNABORTED : 103));
132132
$stream->close();
133133
});
134134

@@ -146,16 +146,19 @@ public function connect($uri)
146146
try {
147147
$response = Psr7\parse_response(substr($buffer, 0, $pos));
148148
} catch (Exception $e) {
149-
$deferred->reject(new RuntimeException('Invalid response received from proxy: ' . $e->getMessage(), 0, $e));
149+
$deferred->reject(new RuntimeException('Invalid response received from proxy (EBADMSG)', defined('SOCKET_EBADMSG') ? SOCKET_EBADMSG: 71, $e));
150150
$stream->close();
151151
return;
152152
}
153153

154-
// status must be 2xx
155-
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
156-
$deferred->reject(new RuntimeException('Proxy rejected with HTTP error code: ' . $response->getStatusCode() . ' ' . $response->getReasonPhrase(), $response->getStatusCode()));
157-
$stream->close();
158-
return;
154+
if ($response->getStatusCode() === 407) {
155+
// map status code 407 (Proxy Authentication Required) to EACCES
156+
$deferred->reject(new RuntimeException('Proxy denied connection due to invalid authentication ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (EACCES)', defined('SOCKET_EACCES') ? SOCKET_EACCES : 13));
157+
return $stream->close();
158+
} elseif ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
159+
// map non-2xx status code to ECONNREFUSED
160+
$deferred->reject(new RuntimeException('Proxy refused connection with HTTP error code ' . $response->getStatusCode() . ' (' . $response->getReasonPhrase() . ') (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111));
161+
return $stream->close();
159162
}
160163

161164
// all okay, resolve with stream instance
@@ -172,23 +175,25 @@ public function connect($uri)
172175

173176
// stop buffering when 8 KiB have been read
174177
if (isset($buffer[8192])) {
175-
$deferred->reject(new RuntimeException('Proxy must not send more than 8 KiB of headers'));
178+
$deferred->reject(new RuntimeException('Proxy must not send more than 8 KiB of headers (EMSGSIZE)', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90));
176179
$stream->close();
177180
}
178181
};
179182
$stream->on('data', $fn);
180183

181184
$stream->on('error', function (Exception $e) use ($deferred) {
182-
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy', 0, $e));
185+
$deferred->reject(new RuntimeException('Stream error while waiting for response from proxy (EIO)', defined('SOCKET_EIO') ? SOCKET_EIO : 5, $e));
183186
});
184187

185188
$stream->on('close', function () use ($deferred) {
186-
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response'));
189+
$deferred->reject(new RuntimeException('Connection to proxy lost while waiting for response (ECONNRESET)', defined('SOCKET_ECONNRESET') ? SOCKET_ECONNRESET : 104));
187190
});
188191

189192
$stream->write("CONNECT " . $host . ":" . $port . " HTTP/1.1\r\nHost: " . $host . ":" . $port . "\r\n" . $auth . "\r\n");
190193

191194
return $deferred->promise();
195+
}, function (Exception $e) use ($proxyUri) {
196+
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
192197
});
193198
}
194199
}

tests/AbstractTestCase.php

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,20 @@ protected function expectCallableOnceWith($value)
3737
return $mock;
3838
}
3939

40+
protected function expectCallableOnceWithExceptionCode($code)
41+
{
42+
$mock = $this->createCallableMock();
43+
$mock
44+
->expects($this->once())
45+
->method('__invoke')
46+
->with($this->callback(function ($e) use ($code) {
47+
return $e->getCode() === $code;
48+
}));
49+
50+
return $mock;
51+
}
52+
53+
4054
protected function expectCallableOnceParameter($type)
4155
{
4256
$mock = $this->createCallableMock();

tests/FunctionalTest.php

+13-3
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,23 @@ public function setUp()
2828
$this->dnsConnector = new DnsConnector($this->tcpConnector, $resolver);
2929
}
3030

31+
public function testNonListeningSocketRejectsConnection()
32+
{
33+
$proxy = new ProxyConnector('127.0.0.1:9999', $this->dnsConnector);
34+
35+
$promise = $proxy->connect('google.com:80');
36+
37+
$this->setExpectedException('RuntimeException', 'Unable to connect to proxy', SOCKET_ECONNREFUSED);
38+
Block\await($promise, $this->loop, 3.0);
39+
}
40+
3141
public function testPlainGoogleDoesNotAcceptConnectMethod()
3242
{
3343
$proxy = new ProxyConnector('google.com', $this->dnsConnector);
3444

3545
$promise = $proxy->connect('google.com:80');
3646

37-
$this->setExpectedException('RuntimeException', 'Method Not Allowed', 405);
47+
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
3848
Block\await($promise, $this->loop, 3.0);
3949
}
4050

@@ -49,7 +59,7 @@ public function testSecureGoogleDoesNotAcceptConnectMethod()
4959

5060
$promise = $proxy->connect('google.com:80');
5161

52-
$this->setExpectedException('RuntimeException', 'Method Not Allowed', 405);
62+
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
5363
Block\await($promise, $this->loop, 3.0);
5464
}
5565

@@ -59,7 +69,7 @@ public function testSecureGoogleDoesNotAcceptPlainStream()
5969

6070
$promise = $proxy->connect('google.com:80');
6171

62-
$this->setExpectedException('RuntimeException', 'Connection to proxy lost');
72+
$this->setExpectedException('RuntimeException', 'Connection to proxy lost', SOCKET_ECONNRESET);
6373
Block\await($promise, $this->loop, 3.0);
6474
}
6575
}

tests/ProxyConnectorTest.php

+33-4
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ public function testRejectsUriWithNonTcpScheme()
162162
$promise->then(null, $this->expectCallableOnce());
163163
}
164164

165+
public function testRejectsIfConnectorRejects()
166+
{
167+
$promise = \React\Promise\reject(new \RuntimeException());
168+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
169+
170+
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
171+
172+
$promise = $proxy->connect('google.com:80');
173+
174+
$promise->then(null, $this->expectCallableOnce());
175+
}
176+
165177
public function testRejectsAndClosesIfStreamWritesNonHttp()
166178
{
167179
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
@@ -176,7 +188,7 @@ public function testRejectsAndClosesIfStreamWritesNonHttp()
176188
$stream->expects($this->once())->method('close');
177189
$stream->emit('data', array("invalid\r\n\r\n"));
178190

179-
$promise->then(null, $this->expectCallableOnce());
191+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
180192
}
181193

182194
public function testRejectsAndClosesIfStreamWritesTooMuchData()
@@ -193,7 +205,24 @@ public function testRejectsAndClosesIfStreamWritesTooMuchData()
193205
$stream->expects($this->once())->method('close');
194206
$stream->emit('data', array(str_repeat('*', 100000)));
195207

196-
$promise->then(null, $this->expectCallableOnce());
208+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EMSGSIZE));
209+
}
210+
211+
public function testRejectsAndClosesIfStreamReturnsProyAuthenticationRequired()
212+
{
213+
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
214+
215+
$promise = \React\Promise\resolve($stream);
216+
$this->connector->expects($this->once())->method('connect')->willReturn($promise);
217+
218+
$proxy = new ProxyConnector('proxy.example.com', $this->connector);
219+
220+
$promise = $proxy->connect('google.com:80');
221+
222+
$stream->expects($this->once())->method('close');
223+
$stream->emit('data', array("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"));
224+
225+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EACCES));
197226
}
198227

199228
public function testRejectsAndClosesIfStreamReturnsNonSuccess()
@@ -210,7 +239,7 @@ public function testRejectsAndClosesIfStreamReturnsNonSuccess()
210239
$stream->expects($this->once())->method('close');
211240
$stream->emit('data', array("HTTP/1.1 403 Not allowed\r\n\r\n"));
212241

213-
$promise->then(null, $this->expectCallableOnce());
242+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
214243
}
215244

216245
public function testResolvesIfStreamReturnsSuccess()
@@ -268,6 +297,6 @@ public function testCancelPromiseWillCloseOpenConnectionAndReject()
268297

269298
$promise->cancel();
270299

271-
$promise->then(null, $this->expectCallableOnce());
300+
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNABORTED));
272301
}
273302
}

0 commit comments

Comments
 (0)