Skip to content
Closed
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
168 changes: 168 additions & 0 deletions src/Query/HostsFileExecutor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php

namespace React\Dns\Query;

use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Dns\Query\ExecutorInterface;
use React\Dns\Query\Query;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Promise\When;
use React\Stream\Stream;

class HostsFileExecutor implements ExecutorInterface
{
private $loop;
private $executor;
private $byName;
private $path;
private $loadingPromise;

public function __construct(LoopInterface $loop, ExecutorInterface $executor, $path = "/etc/hosts")
{
$this->loop = $loop;
$this->executor = $executor;
$this->path = $path;
$this->loadHosts();
}

public function query($nameserver, Query $query)
{
$that = $this;
$executor = $this->executor;

return $this
->loadingPromise
->then(function () use ($that, $query) {
return $that->doQuery($query);
})
->then(null, function () use ($query, $nameserver, $executor) {
return $executor->query($nameserver, $query);
});
}

public function doQuery(Query $query)
{
$deferred = new Deferred();
if (Message::TYPE_A !== $query->type) {
$deferred->reject();
return $deferred->promise();
}

if (!isset($this->byName[$query->name])) {
$deferred->reject();
return $deferred->promise();
}

$records = $this->byName[$query->name];

$response = $this->buildResponse($query, $records);
$deferred->resolve($response);

return $deferred->promise();
}

private function loadHosts()
{
if (null !== $this->loadingPromise) {
return $this->loadingPromise;
}

$this->byName = array();

$deferred = new Deferred();
$this->loadingPromise = $deferred->promise();

$that = $this;

try {

if (!file_exists($this->path)) {
throw new \InvalidArgumentException(sprintf("Hosts file does not exist: %s", $this->path));
}

$fd = fopen($this->path, "rb");

if (!$fd) {
throw new \InvalidArgumentException(sprintf("Unable to open hosts file: %s", $this->path));
}

stream_set_blocking($fd, 0);

$contents = '';

$stream = new Stream($fd, $this->loop);
$stream->on('data', function ($data) use (&$contents, $that) {
$contents = $that->parseHosts($contents . $data);
});
$stream->on('end', function () use (&$contents, $deferred, $that) {
$that->parseHosts($contents . "\n");
$deferred->resolve($contents);
});
$stream->on('error', function ($error) use ($deferred) {
$deferred->reject($error);
});

} catch(\Exception $e) {
$deferred->reject($e);
}
}

public function parseHosts($contents)
{
$offset = 0;
$end = 0;
while (false !== $end = strpos($contents, "\n", $offset)) {

$line = substr($contents, $offset, $end-$offset);
$offset = $end + 1;

if (false !== $i = strpos($line, '#')) {
$line = substr($line, 0, $i);
}

$fields = preg_split("#[ \t]+#", $line, -1, PREG_SPLIT_NO_EMPTY);

if (count($fields) < 2) {
continue;
}

$addr = $fields[0];

if (false === filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
continue;
}

for ($i = 1, $l = count($fields); $i < $l; ++$i) {
$h = $fields[$i];
$this->byName[$h][] = new Record($h, Message::TYPE_A, Message::CLASS_IN, 300, $addr);
}
}

return substr($contents, $offset);
}

public function buildResponse(Query $query, array $records)
{
$response = new Message();

$response->header->set('id', $this->generateId());
$response->header->set('qr', 1);
$response->header->set('opcode', Message::OPCODE_QUERY);
$response->header->set('rd', 1);
$response->header->set('rcode', Message::RCODE_OK);

$response->questions[] = new Record($query->name, $query->type, $query->class);
$response->answers = $records;

$response->prepare();

return $response;
}

protected function generateId()
{
return mt_rand(0, 0xffff);
}
}
10 changes: 8 additions & 2 deletions src/Resolver/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
use React\Dns\Protocol\BinaryDumper;
use React\EventLoop\LoopInterface;
use React\Dns\Query\RetryExecutor;
use React\Dns\Query\HostsFileExecutor;

class Factory
{
public function create($nameserver, LoopInterface $loop)
{
$nameserver = $this->addPortToServerIfMissing($nameserver);
$executor = $this->createRetryExecutor($loop);
$executor = $this->createHostsFileExecutor($loop);

return new Resolver($nameserver, $executor);
}
Expand All @@ -39,9 +40,14 @@ protected function createRetryExecutor(LoopInterface $loop)
return new RetryExecutor($this->createExecutor($loop));
}

protected function createHostsFileExecutor(LoopInterface $loop)
{
return new HostsFileExecutor($loop, $this->createRetryExecutor($loop));
}

protected function createCachedExecutor(LoopInterface $loop)
{
return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache(new ArrayCache()));
return new CachedExecutor($this->createHostsFileExecutor($loop), new RecordCache(new ArrayCache()));
}

protected function addPortToServerIfMissing($nameserver)
Expand Down
13 changes: 13 additions & 0 deletions tests/Fixtures/etc/hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

127.0.0.1 localhost

# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
ff02::3 ip6-allhosts

93.184.216.119 example.com
2606:2800:220:6d:26bf:1447:1097:aa7 example.com
110 changes: 110 additions & 0 deletions tests/Query/HostsFileExecutorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace React\Tests\Dns\Query;

use React\Dns\Model\Message;
use React\Dns\Query\HostsFileExecutor;
use React\Dns\Query\Query;

class HostsFileExecutorTest extends \PHPUnit_Framework_TestCase
{
/**
* @covers React\Dns\Query\HostsFileExecutor
*/
public function testQueryShouldUseFilesystem()
{
$triggerListener = null;
$capturedResponse = null;
$query = new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time());

$loop = $this->getMock('React\EventLoop\LoopInterface');
$loop
->expects($this->once())
->method('addReadStream')
->will($this->returnCallback(function ($stream, $listener) use (&$triggerListener) {
$triggerListener = function () use ($stream, $listener) {
call_user_func($listener, $stream);
};
}));

$fallback = $this->getMock('React\Dns\Query\ExecutorInterface');

$factory = new HostsFileExecutor($loop, $fallback, __DIR__.'/../Fixtures/etc/hosts');

$factory->query('8.8.8.8', $query)->then(function ($response) use (&$capturedResponse) {
$capturedResponse = $response;
});

$triggerListener();

$this->assertNotNull($capturedResponse);
$this->assertCount(1, $capturedResponse->answers);
$this->assertSame('127.0.0.1', $capturedResponse->answers[0]->data);
}

/**
* @covers React\Dns\Query\HostsFileExecutor
*/
public function testQueryShouldFallbackIfFileCannotBeRead()
{
$triggerListener = null;
$capturedResponse = null;
$query = new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time());
$expectedResponse = new Message;

$loop = $this->getMock('React\EventLoop\LoopInterface');

$fallback = $this->getMock('React\Dns\Query\ExecutorInterface');
$fallback
->expects($this->once())
->method('query')
->with('8.8.8.8', $query)
->will($this->returnValue($expectedResponse));

$factory = new HostsFileExecutor($loop, $fallback, __DIR__.'/../Fixtures/unexistant');

$factory->query('8.8.8.8', $query)->then(function ($response) use (&$capturedResponse) {
$capturedResponse = $response;
});

$this->assertSame($expectedResponse, $capturedResponse);
}

/**
* @covers React\Dns\Query\HostsFileExecutor
*/
public function testQueryShouldFallbackIfNameNotFoundInFile()
{
$triggerListener = null;
$capturedResponse = null;
$query = new Query('unexistant.example.com', Message::TYPE_A, Message::CLASS_IN, time());
$expectedResponse = new Message;

$loop = $this->getMock('React\EventLoop\LoopInterface');
$loop
->expects($this->once())
->method('addReadStream')
->will($this->returnCallback(function ($stream, $listener) use (&$triggerListener) {
$triggerListener = function () use ($stream, $listener) {
call_user_func($listener, $stream);
};
}));

$fallback = $this->getMock('React\Dns\Query\ExecutorInterface');
$fallback
->expects($this->once())
->method('query')
->with('8.8.8.8', $query)
->will($this->returnValue($expectedResponse));

$factory = new HostsFileExecutor($loop, $fallback, __DIR__.'/../Fixtures/etc/hosts');

$factory->query('8.8.8.8', $query)->then(function ($response) use (&$capturedResponse) {
$capturedResponse = $response;
});

$triggerListener();

$this->assertSame($expectedResponse, $capturedResponse);
}
}