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
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ jobs:
- run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy
if: ${{ matrix.php < 7.3 }}

PHPUnit-macOS:
name: PHPUnit (macOS)
runs-on: macos-10.15
continue-on-error: true
steps:
- uses: actions/checkout@v2
- uses: shivammathur/setup-php@v2
with:
php-version: 8.0
coverage: xdebug
- run: composer install
- run: vendor/bin/phpunit --coverage-text

PHPUnit-hhvm:
name: PHPUnit (HHVM)
runs-on: ubuntu-18.04
Expand Down
32 changes: 28 additions & 4 deletions src/Query/UdpTransportExecutor.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ final class UdpTransportExecutor implements ExecutorInterface
private $parser;
private $dumper;

/**
* maximum UDP packet size to send and receive
*
* @var int
*/
private $maxPacketSize = 512;

/**
* @param string $nameserver
* @param LoopInterface $loop
Expand Down Expand Up @@ -119,7 +126,7 @@ public function query(Query $query)
$request = Message::createRequestForQuery($query);

$queryData = $this->dumper->toBinary($request);
if (isset($queryData[512])) {
if (isset($queryData[$this->maxPacketSize])) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->name . ' failed: Query too large for UDP transport',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
Expand All @@ -137,7 +144,20 @@ public function query(Query $query)

// set socket to non-blocking and immediately try to send (fill write buffer)
\stream_set_blocking($socket, false);
\fwrite($socket, $queryData);
$written = @\fwrite($socket, $queryData);

if ($written !== \strlen($queryData)) {
// Write may potentially fail, but most common errors are already caught by connection check above.
// Among others, macOS is known to report here when trying to send to broadcast address.
// This can also be reproduced by writing data exceeding `stream_set_chunk_size()` to a server refusing UDP data.
// fwrite(): send of 8192 bytes failed with errno=111 Connection refused
$error = \error_get_last();
\preg_match('/errno=(\d+) (.+)/', $error['message'], $m);
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->name . ' failed: Unable to send query to DNS server (' . (isset($m[2]) ? $m[2] : $error['message']) . ')',
isset($m[1]) ? (int) $m[1] : 0
));
}

$loop = $this->loop;
$deferred = new Deferred(function () use ($loop, $socket, $query) {
Expand All @@ -148,11 +168,15 @@ public function query(Query $query)
throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled');
});

$max = $this->maxPacketSize;
$parser = $this->parser;
$loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request) {
$loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max) {
// try to read a single data packet from the DNS server
// ignoring any errors, this is uses UDP packets and not a stream of data
$data = @\fread($socket, 512);
$data = @\fread($socket, $max);
if ($data === false) {
return;
}

try {
$response = $parser->parseMessage($data);
Expand Down
91 changes: 69 additions & 22 deletions tests/Query/UdpTransportExecutorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,75 @@ public function testQueryRejectsIfServerConnectionFails()
$promise = $executor->query($query);

$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
$promise->then(null, $this->expectCallableOnce());

$exception = null;
$promise->then(null, function ($reason) use (&$exception) {
$exception = $reason;
});

// PHP (Failed to parse address "///") differs from HHVM (Name or service not known)
$this->setExpectedException('RuntimeException', 'Unable to connect to DNS server');
throw $exception;
}

public function testQueryRejectsIfSendToServerFailsAfterConnection()
{
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->never())->method('addReadStream');

$executor = new UdpTransportExecutor('0.0.0.0', $loop);

// increase hard-coded maximum packet size to allow sending excessive data
$ref = new \ReflectionProperty($executor, 'maxPacketSize');
$ref->setAccessible(true);
$ref->setValue($executor, PHP_INT_MAX);

$query = new Query(str_repeat('a.', 100000) . '.example', Message::TYPE_A, Message::CLASS_IN);
$promise = $executor->query($query);

$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);

$exception = null;
$promise->then(null, function ($reason) use (&$exception) {
$exception = $reason;
});

// ECONNREFUSED( Connection refused) on Linux, EMSGSIZE (Message too long) on macOS
$this->setExpectedException('RuntimeException', 'Unable to send query to DNS server');
throw $exception;
}

public function testQueryKeepsPendingIfReadFailsBecauseServerRefusesConnection()
{
$socket = null;
$callback = null;
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
$loop->expects($this->once())->method('addReadStream')->with($this->callback(function ($ref) use (&$socket) {
$socket = $ref;
return true;
}), $this->callback(function ($ref) use (&$callback) {
$callback = $ref;
return true;
}));

$executor = new UdpTransportExecutor('0.0.0.0', $loop);

$query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN);
$promise = $executor->query($query);

$this->assertNotNull($socket);
$callback($socket);

$this->assertInstanceOf('React\Promise\PromiseInterface', $promise);

$pending = true;
$promise->then(function () use (&$pending) {
$pending = false;
}, function () use (&$pending) {
$pending = false;
});

$this->assertTrue($pending);
}

/**
Expand All @@ -142,27 +210,6 @@ public function testQueryRejectsOnCancellation()
$promise->then(null, $this->expectCallableOnce());
}

public function testQueryKeepsPendingIfServerRejectsNetworkPacket()
{
$loop = Factory::create();

$executor = new UdpTransportExecutor('127.0.0.1:1', $loop);

$query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);

$wait = true;
$promise = $executor->query($query)->then(
null,
function ($e) use (&$wait) {
$wait = false;
throw $e;
}
);

\Clue\React\Block\sleep(0.2, $loop);
$this->assertTrue($wait);
}

public function testQueryKeepsPendingIfServerSendsInvalidMessage()
{
$loop = Factory::create();
Expand Down