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:
parent
1d418d3e82
commit
d5b5192ba3
3
.idea/vcs.xml
generated
3
.idea/vcs.xml
generated
@ -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
26
examples/basic_run.php
Normal 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
10
lib/Addr/AddressModes.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Addr;
|
||||
|
||||
class AddressModes
|
||||
{
|
||||
const INET4_ADDR = 1;
|
||||
const INET6_ADDR = 2;
|
||||
const PREFER_INET6 = 4;
|
||||
}
|
@ -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
213
lib/Addr/Client.php
Normal 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
129
lib/Addr/HostsFile.php
Normal 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;
|
||||
}
|
||||
}
|
24
lib/Addr/NameValidator.php
Normal file
24
lib/Addr/NameValidator.php
Normal 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);
|
||||
}
|
||||
}
|
64
lib/Addr/RequestBuilder.php
Normal file
64
lib/Addr/RequestBuilder.php
Normal 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);
|
||||
}
|
||||
}
|
10
lib/Addr/ResolutionErrors.php
Normal file
10
lib/Addr/ResolutionErrors.php
Normal 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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
52
lib/Addr/ResponseInterpreter.php
Normal file
52
lib/Addr/ResponseInterpreter.php
Normal 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()];
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user