Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use socket error codes for connection rejections #17

Merged
merged 3 commits into from
Aug 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,8 @@ $proxy = new ProxyConnector(
connection attempt.
If the authentication details are missing or not accepted by the remote HTTP
proxy server, it is expected to reject each connection attempt with a
`407` (Proxy Authentication Required) response status code.
`407` (Proxy Authentication Required) response status code and an exception
error code of `SOCKET_EACCES` (13).

#### Advanced secure proxy connections

Expand Down
25 changes: 15 additions & 10 deletions src/ProxyConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public function connect($uri)

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

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

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

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

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

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

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

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

return $deferred->promise();
}, function (Exception $e) use ($proxyUri) {
throw new RuntimeException('Unable to connect to proxy (ECONNREFUSED)', defined('SOCKET_ECONNREFUSED') ? SOCKET_ECONNREFUSED : 111, $e);
});
}
}
14 changes: 14 additions & 0 deletions tests/AbstractTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ protected function expectCallableOnceWith($value)
return $mock;
}

protected function expectCallableOnceWithExceptionCode($code)
{
$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->callback(function ($e) use ($code) {
return $e->getCode() === $code;
}));

return $mock;
}


protected function expectCallableOnceParameter($type)
{
$mock = $this->createCallableMock();
Expand Down
16 changes: 13 additions & 3 deletions tests/FunctionalTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,23 @@ public function setUp()
$this->dnsConnector = new DnsConnector($this->tcpConnector, $resolver);
}

public function testNonListeningSocketRejectsConnection()
{
$proxy = new ProxyConnector('127.0.0.1:9999', $this->dnsConnector);

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

$this->setExpectedException('RuntimeException', 'Unable to connect to proxy', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}

public function testPlainGoogleDoesNotAcceptConnectMethod()
{
$proxy = new ProxyConnector('google.com', $this->dnsConnector);

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

$this->setExpectedException('RuntimeException', 'Method Not Allowed', 405);
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}

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

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

$this->setExpectedException('RuntimeException', 'Method Not Allowed', 405);
$this->setExpectedException('RuntimeException', '405 (Method Not Allowed)', SOCKET_ECONNREFUSED);
Block\await($promise, $this->loop, 3.0);
}

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

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

$this->setExpectedException('RuntimeException', 'Connection to proxy lost');
$this->setExpectedException('RuntimeException', 'Connection to proxy lost', SOCKET_ECONNRESET);
Block\await($promise, $this->loop, 3.0);
}
}
37 changes: 33 additions & 4 deletions tests/ProxyConnectorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,18 @@ public function testRejectsUriWithNonTcpScheme()
$promise->then(null, $this->expectCallableOnce());
}

public function testRejectsIfConnectorRejects()
{
$promise = \React\Promise\reject(new \RuntimeException());
$this->connector->expects($this->once())->method('connect')->willReturn($promise);

$proxy = new ProxyConnector('proxy.example.com', $this->connector);

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

$promise->then(null, $this->expectCallableOnce());
}

public function testRejectsAndClosesIfStreamWritesNonHttp()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();
Expand All @@ -176,7 +188,7 @@ public function testRejectsAndClosesIfStreamWritesNonHttp()
$stream->expects($this->once())->method('close');
$stream->emit('data', array("invalid\r\n\r\n"));

$promise->then(null, $this->expectCallableOnce());
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EBADMSG));
}

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

$promise->then(null, $this->expectCallableOnce());
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EMSGSIZE));
}

public function testRejectsAndClosesIfStreamReturnsProyAuthenticationRequired()
{
$stream = $this->getMockBuilder('React\Socket\Connection')->disableOriginalConstructor()->setMethods(array('close', 'write'))->getMock();

$promise = \React\Promise\resolve($stream);
$this->connector->expects($this->once())->method('connect')->willReturn($promise);

$proxy = new ProxyConnector('proxy.example.com', $this->connector);

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

$stream->expects($this->once())->method('close');
$stream->emit('data', array("HTTP/1.1 407 Proxy Authentication Required\r\n\r\n"));

$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_EACCES));
}

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

$promise->then(null, $this->expectCallableOnce());
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNREFUSED));
}

public function testResolvesIfStreamReturnsSuccess()
Expand Down Expand Up @@ -268,6 +297,6 @@ public function testCancelPromiseWillCloseOpenConnectionAndReject()

$promise->cancel();

$promise->then(null, $this->expectCallableOnce());
$promise->then(null, $this->expectCallableOnceWithExceptionCode(SOCKET_ECONNABORTED));
}
}