1
0
mirror of https://github.com/danog/dns.git synced 2025-01-22 05:21:10 +01:00

Finished initial implementation

zOMG too many things to document
This commit is contained in:
Chris Wright 2014-06-15 00:47:15 +01:00
parent 1d418d3e82
commit d5b5192ba3
13 changed files with 773 additions and 92 deletions

3
.idea/vcs.xml generated
View File

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/vendor/Alert" vcs="Git" />
<mapping directory="$PROJECT_DIR$/vendor/LibDNS" vcs="Git" />
</component>
</project>

26
examples/basic_run.php Normal file
View File

@ -0,0 +1,26 @@
<?php
use Addr\ResolverFactory,
Alert\ReactorFactory;
require dirname(__DIR__) . '/src/bootstrap.php';
$names = [
'google.com',
'github.com',
'stackoverflow.com',
'localhost',
'192.168.0.1',
'::1',
];
$reactor = (new ReactorFactory)->select();
$resolver = (new ResolverFactory)->createResolver($reactor);
foreach ($names as $name) {
$resolver->resolve($name, function($addr) use($name, $resolver) {
echo "{$name}: {$addr}\n";
});
}
$reactor->run();

10
lib/Addr/AddressModes.php Normal file
View File

@ -0,0 +1,10 @@
<?php
namespace Addr;
class AddressModes
{
const INET4_ADDR = 1;
const INET6_ADDR = 2;
const PREFER_INET6 = 4;
}

View File

@ -5,24 +5,69 @@ namespace Addr;
class Cache
{
/**
* @param string $name
* @return string|null
* @todo
* Mapped names stored in the cache
*
* @var array
*/
public function fetch($name)
{
private $data = [
AddressModes::INET4_ADDR => [],
AddressModes::INET6_ADDR => [],
];
/**
* Look up a name in the cache
*
* @param string $name
* @param int $mode
* @return string|null
*/
public function resolve($name, $mode)
{
$have4 = isset($this->data[AddressModes::INET4_ADDR][$name]);
$have6 = isset($this->data[AddressModes::INET6_ADDR][$name]);
if ($have6 && (!$have4 || $mode & AddressModes::PREFER_INET6)) {
$type = AddressModes::INET6_ADDR;
} else if ($have4) {
$type = AddressModes::INET4_ADDR;
} else {
return null;
}
if ($this->data[$type][$name][1] < time()) {
unset($this->data[$type][$name]);
return null;
}
return [$this->data[$type][$name][0], $type];
}
/**
* Store an entry in the cache
*
* @param string $name
* @param string $addr
* @param int $type
* @param int $ttl
* @return string|null
* @todo
*/
public function store($name, $addr, $ttl)
public function store($name, $addr, $type, $ttl)
{
$this->data[$type][$name] = [$addr, time() + $ttl];
}
/**
* Remove expired records from the cache
*/
public function collectGarbage()
{
$now = time();
foreach ([AddressModes::INET4_ADDR, AddressModes::INET6_ADDR] as $type) {
while (list($name, $data) = each($this->data[$type])) {
if ($data[1] < $now) {
unset($this->data[$type][$name]);
}
}
}
}
}

213
lib/Addr/Client.php Normal file
View File

@ -0,0 +1,213 @@
<?php
namespace Addr;
use Alert\Reactor;
class Client
{
/**
* @var Reactor
*/
private $reactor;
/**
* @var RequestBuilder
*/
private $requestBuilder;
/**
* @var ResponseInterpreter
*/
private $responseInterpreter;
/**
* @var resource
*/
private $socket;
/**
* @var int
*/
private $requestTimeout;
/**
* @var int
*/
private $readWatcherId;
/**
* @var array
*/
private $outstandingRequests = [];
/**
* @var int
*/
private $requestIdCounter = 0;
/**
* Constructor
*
* @param Reactor $reactor
* @param RequestBuilder $requestBuilder
* @param ResponseInterpreter $responseInterpreter
* @param string $serverAddress
* @param int $serverPort
* @param int $requestTimeout
* @throws \RuntimeException
*/
public function __construct(
Reactor $reactor,
RequestBuilder $requestBuilder,
ResponseInterpreter $responseInterpreter,
$serverAddress = '8.8.8.8',
$serverPort = 53,
$requestTimeout = 2000
) {
$this->reactor = $reactor;
$this->requestBuilder = $requestBuilder;
$this->responseInterpreter = $responseInterpreter;
$address = sprintf('udp://%s:%d', $serverAddress, $serverPort);
$this->socket = stream_socket_client($address, $errNo, $errStr);
if (!$this->socket) {
throw new \RuntimeException("Creating socket {$address} failed: {$errNo}: {$errStr}");
}
stream_set_blocking($this->socket, 0);
$this->requestTimeout = $requestTimeout;
}
/**
* Get the next available request ID
*
* @return int
*/
private function getNextFreeRequestId()
{
do {
$result = $this->requestIdCounter++;
if ($this->requestIdCounter >= 65536) {
$this->requestIdCounter = 0;
}
} while(isset($this->outstandingRequests[$result]));
return $result;
}
/**
* Get a list of requests to execute for a given mode mask
*
* @param int $mode
* @return array
*/
private function getRequestList($mode)
{
$result = [];
if ($mode & AddressModes::PREFER_INET6) {
if ($mode & AddressModes::INET6_ADDR) {
$result[] = AddressModes::INET6_ADDR;
}
if ($mode & AddressModes::INET4_ADDR) {
$result[] = AddressModes::INET4_ADDR;
}
} else {
if ($mode & AddressModes::INET4_ADDR) {
$result[] = AddressModes::INET4_ADDR;
}
if ($mode & AddressModes::INET6_ADDR) {
$result[] = AddressModes::INET6_ADDR;
}
}
return $result;
}
/**
* Handle data waiting to be read from the socket
*/
private function onSocketReadable()
{
$packet = fread($this->socket, 512);
$response = $this->responseInterpreter->interpret($packet);
if ($response === null) {
return;
}
list($id, $addr, $ttl) = $response;
if ($addr !== null) {
$this->completeOutstandingRequest($id, $addr, $this->outstandingRequests[$id][2], $ttl);
} else {
$this->processOutstandingRequest($id);
}
}
/**
* Call a response callback with the result
*
* @param int $id
* @param string $addr
* @param int $type
* @param int $ttl
*/
private function completeOutstandingRequest($id, $addr, $type, $ttl = null)
{
$this->reactor->cancel($this->outstandingRequests[$id][3]);
call_user_func($this->outstandingRequests[$id][4], $addr, $type, $ttl);
unset($this->outstandingRequests[$id]);
if (!$this->outstandingRequests) {
$this->reactor->cancel($this->readWatcherId);
$this->readWatcherId = null;
}
}
/**
* Send a request to the server
*
* @param int $id
*/
private function processOutstandingRequest($id)
{
if (!$this->outstandingRequests[$id][1]) {
$this->completeOutstandingRequest($id, null, ResolutionErrors::ERR_NO_RECORD);
return;
}
$type = array_shift($this->outstandingRequests[$id][1]);
$this->outstandingRequests[$id][2] = $type;
$packet = $this->requestBuilder->buildRequest($id, $this->outstandingRequests[$id][0], $type);
fwrite($this->socket, $packet);
$this->outstandingRequests[$id][3] = $this->reactor->once(function() use($id) {
$this->completeOutstandingRequest($id, null, ResolutionErrors::ERR_SERVER_TIMEOUT);
}, $this->requestTimeout);
if ($this->readWatcherId === null) {
$this->readWatcherId = $this->reactor->onReadable($this->socket, function() {
$this->onSocketReadable();
});
}
}
/**
* Resolve a name from a server
*
* @param string $name
* @param int $mode
* @param callable $callback
*/
public function resolve($name, $mode, callable $callback)
{
$requests = $this->getRequestList($mode);
$id = $this->getNextFreeRequestId();
$this->outstandingRequests[$id] = [$name, $requests, null, null, $callback];
$this->processOutstandingRequest($id);
}
}

129
lib/Addr/HostsFile.php Normal file
View File

@ -0,0 +1,129 @@
<?php
namespace Addr;
class HostsFile
{
/**
* @var NameValidator
*/
private $nameValidator;
/**
* Path to hosts file
*
* @var string
*/
private $path;
/**
* Mapped names from hosts file
*
* @var array
*/
private $data;
/**
* The file modification time when the last reload was performed
*
* @var int
*/
private $lastModTime = 0;
/**
* Constructor
*
* @param NameValidator $nameValidator
* @param string $path
* @throws \LogicException
*/
public function __construct(NameValidator $nameValidator, $path = null)
{
$this->nameValidator = $nameValidator;
if ($path === null) {
$path = stripos(PHP_OS, 'win') === 0 ? 'C:\Windows\system32\drivers\etc\hosts' : '/etc/hosts';
}
if (!file_exists($path)) {
throw new \LogicException($path . ' does not exist');
} else if (!is_file($path)) {
throw new \LogicException($path . ' is not a file');
} else if (!is_readable($path)) {
throw new \LogicException($path . ' is not readable');
}
$this->path = $path;
}
/**
* Parse a hosts file into an array
*/
private function reload()
{
$this->data = [
AddressModes::INET4_ADDR => [],
AddressModes::INET6_ADDR => [],
];
$key = null;
foreach (file($this->path) as $line) {
$line = trim($line);
if ($line[0] === '#') {
continue;
}
$parts = preg_split('/\s+/', $line);
if (!filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$key = AddressModes::INET4_ADDR;
} else if (!filter_var($parts[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$key = AddressModes::INET6_ADDR;
} else {
continue;
}
for ($i = 1, $l = count($parts); $i < $l; $i++) {
if ($this->nameValidator->validate($parts[$i])) {
$this->data[$key][$parts[$i]] = $parts[0];
}
}
}
}
/**
* Ensure the loaded data is current
*/
private function ensureDataIsCurrent()
{
clearstatcache(true, $this->path);
$modTime = filemtime($this->path);
if ($modTime > $this->lastModTime) {
$this->reload();
$this->lastModTime = $modTime;
}
}
/**
* Look up a name in the hosts file
*
* @param string $name
* @param int $mode
* @return array|null
*/
public function resolve($name, $mode)
{
$this->ensureDataIsCurrent();
$have4 = isset($this->data[AddressModes::INET4_ADDR][$name]);
$have6 = isset($this->data[AddressModes::INET6_ADDR][$name]);
if ($have6 && (!$have4 || $mode & AddressModes::PREFER_INET6)) {
return [$this->data[AddressModes::INET6_ADDR][$name], AddressModes::INET6_ADDR];
} else if ($have4) {
return [$this->data[AddressModes::INET4_ADDR][$name], AddressModes::INET4_ADDR];
}
return null;
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace Addr;
class NameValidator
{
/**
* Regex for validating domain name format
*
* @var string
*/
private $validatePattern = '/^(?:[a-z][a-z0-9\-]{0,61}[a-z0-9])(?:\.[a-z][a-z0-9\-]{0,61}[a-z0-9])*$/i';
/**
* Check that a name is valid
*
* @param string $name
* @return bool
*/
public function validate($name)
{
return strlen($name) <= 253 && preg_match($this->validatePattern, $name);
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Addr;
use LibDNS\Messages\MessageFactory,
LibDNS\Messages\MessageTypes,
LibDNS\Records\QuestionFactory,
LibDNS\Records\ResourceQTypes,
LibDNS\Encoder\Encoder;
class RequestBuilder
{
/**
* @var MessageFactory
*/
private $messageFactory;
/**
* @var QuestionFactory
*/
private $questionFactory;
/**
* @var Encoder
*/
private $encoder;
/**
* Constructor
*
* @param MessageFactory $messageFactory
* @param QuestionFactory $questionFactory
* @param Encoder $encoder
*/
public function __construct(MessageFactory $messageFactory, QuestionFactory $questionFactory, Encoder $encoder)
{
$this->messageFactory = $messageFactory;
$this->questionFactory = $questionFactory;
$this->encoder = $encoder;
}
/**
* Build a request packet for a name and record type
*
* @param int $id
* @param string $name
* @param int $type
* @return string
*/
public function buildRequest($id, $name, $type)
{
$qType = $type === AddressModes::INET4_ADDR ? ResourceQTypes::A : ResourceQTypes::AAAA;
$question = $this->questionFactory->create($qType);
$question->setName($name);
$request = $this->messageFactory->create(MessageTypes::QUERY);
$request->setID($id);
$request->getQuestionRecords()->add($question);
$request->isRecursionDesired(true);
return $this->encoder->encode($request);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Addr;
class ResolutionErrors
{
const ERR_INVALID_NAME = 1;
const ERR_NO_RECORD = 2;
const ERR_SERVER_TIMEOUT = 3;
}

View File

@ -6,109 +6,72 @@ use Alert\Reactor;
class Resolver
{
const INET4_ADDR = 1;
const INET6_ADDR = 2;
const PREFER_INET4 = 4;
private $domainNameMatchExpr = '/^(?:[a-z][a-z0-9\-]{0,61}[a-z0-9])(?:\.[a-z][a-z0-9\-]{0,61}[a-z0-9])*/i';
/**
* @var Reactor
*/
private $reactor;
/**
* @var NameValidator
*/
private $nameValidator;
/**
* @var Client
*/
private $client;
/**
* @var Cache
*/
private $cache;
private $hostsFilePath;
private $hostsFileLastModTime = 0;
private $hostsFileData = [];
/**
* @param Reactor $reactor
* @param Cache $cache
* @var HostsFile
*/
public function __construct(Reactor $reactor, Cache $cache = null)
{
$this->reactor = $reactor;
$path = stripos(PHP_OS, 'win') === 0 ? 'C:\Windows\system32\drivers\etc\hosts' : '/etc/hosts';
if (is_file($path) && is_readable($path)) {
$this->hostsFilePath = $path;
}
}
private $hostsFile;
/**
* Parse a hosts file into an array
*/
private function reloadHostsFileData()
{
$this->hostsFileData = [];
foreach (file($this->hostsFilePath) as $line) {
$line = trim($line);
if ($line[0] === '#') {
continue;
}
$parts = preg_split('/\s+/', $line);
if (!filter_var($parts[0], FILTER_VALIDATE_IP)) {
continue;
}
for ($i = 1, $l = count($parts); $i < $l; $i++) {
if (preg_match($this->domainNameMatchExpr, $parts[$i])) {
$this->hostsFileData[$parts[$i]] = $parts[0];
}
}
}
}
/**
* Lookup a name in the hosts file
* Constructor
*
* @param string $name
* @return string|null
* @param Reactor $reactor
* @param NameValidator $nameValidator
* @param Client $client
* @param Cache $cache
* @param HostsFile $hostsFile
*/
private function resolveFromHostsFile($name)
{
if ($this->hostsFilePath) {
clearstatcache(true, $this->hostsFilePath);
$mtime = filemtime($this->hostsFilePath);
if ($mtime > $this->hostsFileLastModTime) {
$this->reloadHostsFileData();
}
}
return isset($this->hostsFileData[$name]) ? $this->hostsFileData[$name] : null;
public function __construct(
Reactor $reactor,
NameValidator $nameValidator,
Client $client = null,
Cache $cache = null,
HostsFile $hostsFile = null
) {
$this->reactor = $reactor;
$this->nameValidator = $nameValidator;
$this->client = $client;
$this->cache = $cache;
$this->hostsFile = $hostsFile;
}
/**
* Try and resolve a name through the hosts file or the cache
* Check if a supplied name is an IP address and resolve immediately
*
* @param string $name
* @param callable $callback
* @param int $mode
* @return bool
*/
private function resolveNameLocally($name, callable $callback, $mode = 7)
private function resolveAsIPAddress($name, $callback)
{
if (null !== $result = $this->resolveFromHostsFile($name)) {
$this->reactor->immediately(function() use($callback, $result) {
call_user_func($callback, $result);
if (filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
$this->reactor->immediately(function() use($callback, $name) {
call_user_func($callback, $name, AddressModes::INET4_ADDR);
});
return true;
}
if ($this->cache && null !== $result = $this->cache->fetch($name)) {
$this->reactor->immediately(function() use($callback, $result) {
call_user_func($callback, $result);
} else if (filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
$this->reactor->immediately(function() use($callback, $name) {
call_user_func($callback, $name, AddressModes::INET6_ADDR);
});
return true;
@ -118,14 +81,117 @@ class Resolver
}
/**
* Resolve a name in the hosts file
*
* @param string $name
* @param int $mode
* @param callable $callback
* @return bool
*/
public function resolve($name, callable $callback)
private function resolveInHostsFile($name, $mode, $callback)
{
if ($this->resolveNameLocally($name, $callback)) {
/* localhost should resolve regardless of whether we have a hosts file
also the Windows hosts file no longer contains this record */
if ($name === 'localhost') {
if ($mode & AddressModes::PREFER_INET6) {
$this->reactor->immediately(function() use($callback) {
call_user_func($callback, '::1', AddressModes::INET6_ADDR);
});
} else {
$this->reactor->immediately(function() use($callback) {
call_user_func($callback, '127.0.0.1', AddressModes::INET4_ADDR);
});
}
return true;
}
if (!$this->hostsFile || null === $result = $this->hostsFile->resolve($name, $mode)) {
return false;
}
list($addr, $type) = $result;
$this->reactor->immediately(function() use($callback, $addr, $type) {
call_user_func($callback, $addr, $type);
});
return true;
}
/**
* Resolve a name in the cache
*
* @param string $name
* @param int $mode
* @param callable $callback
* @return bool
*/
private function resolveInCache($name, $mode, $callback)
{
if ($this->cache && null !== $result = $this->cache->resolve($name, $mode)) {
list($addr, $type) = $result;
$this->reactor->immediately(function() use($callback, $addr, $type) {
call_user_func($callback, $addr, $type);
});
return true;
}
return false;
}
/**
* Resolve a name from a server
*
* @param string $name
* @param int $mode
* @param callable $callback
* @return bool
*/
private function resolveFromServer($name, $mode, $callback)
{
if (!$this->client) {
$this->reactor->immediately(function() use($callback) {
call_user_func($callback, null, ResolutionErrors::ERR_NO_RECORD);
});
return;
}
$this->client->resolve($name, $mode, function($addr, $type, $ttl) use($name, $callback) {
if ($addr !== null && $this->cache) {
$this->cache->store($name, $addr, $type, $ttl);
}
call_user_func($callback, $addr, $type);
});
}
/**
* Resolve a name
*
* @param string $name
* @param callable $callback
* @param int $mode
*/
public function resolve($name, callable $callback, $mode = 3)
{
if ($this->resolveAsIPAddress($name, $callback)) {
return;
}
if (!$this->nameValidator->validate($name)) {
$this->reactor->immediately(function() use($callback) {
call_user_func($callback, null, ResolutionErrors::ERR_INVALID_NAME);
});
return;
}
if ($this->resolveInHostsFile($name, $mode, $callback) || $this->resolveInCache($name, $mode, $callback)) {
return;
}
$this->resolveFromServer($name, $mode, $callback);
}
}

View File

@ -2,7 +2,11 @@
namespace Addr;
use Alert\Reactor;
use Alert\Reactor,
LibDNS\Decoder\DecoderFactory,
LibDNS\Encoder\EncoderFactory,
LibDNS\Messages\MessageFactory,
LibDNS\Records\QuestionFactory;
class ResolverFactory
{
@ -10,10 +14,37 @@ class ResolverFactory
* Create a new resolver instance
*
* @param Reactor $reactor
* @param string $serverAddr
* @param int $serverPort
* @param int $requestTimeout
* @param string $hostsFilePath
* @return Resolver
*/
public function createResolver(Reactor $reactor)
{
return new Resolver($reactor, new Cache);
public function createResolver(
Reactor $reactor,
$serverAddr = '8.8.8.8',
$serverPort = 53,
$requestTimeout = 2000,
$hostsFilePath = null
) {
$nameValidator = new NameValidator;
$client = new Client(
$reactor,
new RequestBuilder(
new MessageFactory,
new QuestionFactory,
(new EncoderFactory)->create()
),
new ResponseInterpreter(
(new DecoderFactory)->create()
),
$serverAddr, $serverPort, $requestTimeout
);
$cache = new Cache;
$hostsFile = new HostsFile($nameValidator, $hostsFilePath);
return new Resolver($reactor, $nameValidator, $client, $cache, $hostsFile);
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace Addr;
use LibDNS\Decoder\Decoder,
LibDNS\Messages\MessageTypes;
class ResponseInterpreter
{
/**
* @var Decoder
*/
private $decoder;
/**
* Constructor
*
* @param Decoder $decoder
*/
public function __construct(Decoder $decoder)
{
$this->decoder = $decoder;
}
/**
* Extract the message ID and response data from a DNS response packet
*
* @param string $packet
* @return array|null
*/
public function interpret($packet)
{
try {
$message = $this->decoder->decode($packet);
} catch (\Exception $e) {
return null;
}
if ($message->getType() !== MessageTypes::RESPONSE || $message->getResponseCode() !== 0) {
return null;
}
$answers = $message->getAnswerRecords();
if (!count($answers)) {
return [$message->getID(), null];
}
/** @var \LibDNS\Records\Resource $record */
$record = $answers->getRecordByIndex(0);
return [$message->getID(), (string)$record->getData(), $record->getTTL()];
}
}

View File

@ -1,7 +1,17 @@
<?php
spl_autoload_register(function($className) {
$libRoot = dirname(__DIR__);
spl_autoload_register(function($className) use($libRoot) {
if (strpos($className, 'Addr\\') === 0) {
require dirname(__DIR__) . '/lib/' . strtr($className, '\\', '/') . '.php';
require $libRoot . '/lib/' . strtr($className, '\\', '/') . '.php';
}
});
require $libRoot . '/vendor/Alert/src/bootstrap.php';
spl_autoload_register(function($className) use($libRoot) {
if (strpos($className, 'LibDNS\\') === 0) {
require $libRoot . '/vendor/LibDNS/src/' . strtr($className, '\\', '/') . '.php';
}
});