Skip to content
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
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"require": {
"php": ">=5.3.0",
"react/cache": "~0.4.0|~0.3.0",
"react/socket": "~0.4.0|~0.3.0",
"react/socket": "^0.4.4",
"react/promise": "~2.1|~1.2"
},
"autoload": {
Expand Down
25 changes: 22 additions & 3 deletions src/Query/Executor.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,27 @@ public function doQuery($nameserver, $transport, $queryData, $name)
$deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name)));
});

$conn = $this->createConnection($nameserver, $transport);
try {
try {
$conn = $this->createConnection($nameserver, $transport);
} catch (\Exception $e) {
if ($transport === 'udp') {
// UDP failed => retry with TCP
$transport = 'tcp';
$conn = $this->createConnection($nameserver, $transport);
} else {
// TCP failed (UDP must already have been checked before)
throw $e;
}
}
} catch (\Exception $e) {
// both UDP and TCP failed => reject
$timer->cancel();
$deferred->reject(new \RuntimeException('Unable to connect to DNS server: ' . $e->getMessage(), 0, $e));

return $deferred->promise();
}

$conn->on('data', function ($data) use ($retryWithTcp, $conn, $parser, $response, $transport, $deferred, $timer) {
$responseReady = $parser->parseChunk($data, $response);

Expand Down Expand Up @@ -105,8 +125,7 @@ protected function generateId()

protected function createConnection($nameserver, $transport)
{
$fd = stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
stream_set_blocking($fd, 0);
$fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
$conn = new Connection($fd, $this->loop);

return $conn;
Expand Down
9 changes: 9 additions & 0 deletions tests/FunctionalResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,13 @@ public function testResolveCancelledRejectsImmediately()

$this->assertLessThan(0.1, $time);
}

public function testInvalidResolverDoesNotResolveGoogle()
{
$factory = new Factory();
$this->resolver = $factory->create('255.255.255.255', $this->loop);

$promise = $this->resolver->resolve('google.com');
$promise->then($this->expectCallableNever(), $this->expectCallableOnce());
}
}
75 changes: 73 additions & 2 deletions tests/Query/ExecutorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ public function resolveShouldCloseConnectionWhenCancelled()
/** @test */
public function resolveShouldRetryWithTcpIfResponseIsTruncated()
{
$conn = $this->createConnectionMock();

$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');

$this->loop
Expand Down Expand Up @@ -137,6 +135,79 @@ public function resolveShouldRetryWithTcpIfResponseIsTruncated()
$this->executor->query('8.8.8.8:53', $query, function () {}, function () {});
}

/** @test */
public function resolveShouldRetryWithTcpIfUdpThrows()
{
$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');

$this->loop
->expects($this->once())
->method('addTimer')
->will($this->returnValue($timer));

$this->parser
->expects($this->once())
->method('parseChunk')
->with($this->anything(), $this->isInstanceOf('React\Dns\Model\Message'))
->will($this->returnStandardResponse());

$this->executor = $this->createExecutorMock();
$this->executor
->expects($this->at(0))
->method('createConnection')
->with('8.8.8.8:53', 'udp')
->will($this->throwException(new \Exception()));
$this->executor
->expects($this->at(1))
->method('createConnection')
->with('8.8.8.8:53', 'tcp')
->will($this->returnNewConnectionMock());

$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
$this->executor->query('8.8.8.8:53', $query, function () {}, function () {});
}

/** @test */
public function resolveShouldFailIfBothUdpAndTcpThrow()
{
$timer = $this->getMock('React\EventLoop\Timer\TimerInterface');

$this->loop
->expects($this->once())
->method('addTimer')
->will($this->returnValue($timer));

$this->parser
->expects($this->never())
->method('parseChunk');

$this->executor = $this->createExecutorMock();
$this->executor
->expects($this->at(0))
->method('createConnection')
->with('8.8.8.8:53', 'udp')
->will($this->throwException(new \Exception()));
$this->executor
->expects($this->at(1))
->method('createConnection')
->with('8.8.8.8:53', 'tcp')
->will($this->throwException(new \Exception()));

$query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
$promise = $this->executor->query('8.8.8.8:53', $query, function () {}, function () {});

$mock = $this->createCallableMock();
$mock
->expects($this->once())
->method('__invoke')
->with($this->callback(function($e) {
return $e instanceof \RuntimeException &&
strpos($e->getMessage(), 'Unable to connect to DNS server') === 0;
}));

$promise->then($this->expectCallableNever(), $mock);
}

/** @test */
public function resolveShouldFailIfResponseIsTruncatedAfterTcpRequest()
{
Expand Down