mirror of
https://github.com/danog/dns.git
synced 2024-11-30 04:29:06 +01:00
Refactor config loaders out of DefaultResolver
This commit is contained in:
parent
4d8f27d9b5
commit
25a8110c89
45
lib/Config.php
Normal file
45
lib/Config.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
class Config {
|
||||||
|
private $nameservers;
|
||||||
|
private $knownHosts;
|
||||||
|
private $timeout;
|
||||||
|
private $attempts;
|
||||||
|
|
||||||
|
public function __construct(array $nameservers, array $knownHosts = [], int $timeout = 3000, int $attempts = 2) {
|
||||||
|
if (\count($nameservers) < 1) {
|
||||||
|
throw new ConfigException("At least one nameserver is required for a valid config");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($timeout < 0) {
|
||||||
|
throw new ConfigException("Invalid timeout ({$timeout}), must be 0 or greater");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($attempts < 1) {
|
||||||
|
throw new ConfigException("Invalid attempt count ({$attempts}), must be 1 or greater");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->nameservers = $nameservers;
|
||||||
|
$this->knownHosts = $knownHosts;
|
||||||
|
$this->timeout = $timeout;
|
||||||
|
$this->attempts = $attempts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNameservers(): array {
|
||||||
|
return $this->nameservers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKnownHosts(): array {
|
||||||
|
return $this->knownHosts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTimeout(): int {
|
||||||
|
return $this->timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAttempts(): int {
|
||||||
|
return $this->attempts;
|
||||||
|
}
|
||||||
|
}
|
14
lib/ConfigException.php
Normal file
14
lib/ConfigException.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MUST be thrown in case the config can't be read and no fallback is available.
|
||||||
|
*/
|
||||||
|
class ConfigException extends ResolutionException {
|
||||||
|
public function __construct(string $message, Throwable $previous = null) {
|
||||||
|
parent::__construct($message, 0, $previous);
|
||||||
|
}
|
||||||
|
}
|
9
lib/ConfigLoader.php
Normal file
9
lib/ConfigLoader.php
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
use Amp\Promise;
|
||||||
|
|
||||||
|
interface ConfigLoader {
|
||||||
|
public function loadConfig(): Promise;
|
||||||
|
}
|
@ -2,18 +2,17 @@
|
|||||||
|
|
||||||
namespace Amp\Dns;
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
use Amp;
|
||||||
use Amp\Cache\ArrayCache;
|
use Amp\Cache\ArrayCache;
|
||||||
|
use Amp\Cache\Cache;
|
||||||
use Amp\CallableMaker;
|
use Amp\CallableMaker;
|
||||||
use Amp\Deferred;
|
use Amp\Deferred;
|
||||||
use Amp\Failure;
|
use Amp\Failure;
|
||||||
use Amp\File\FilesystemException;
|
|
||||||
use Amp\Loop;
|
use Amp\Loop;
|
||||||
use Amp\MultiReasonException;
|
use Amp\MultiReasonException;
|
||||||
use Amp\Promise;
|
use Amp\Promise;
|
||||||
use Amp\Success;
|
use Amp\Success;
|
||||||
use Amp\TimeoutException;
|
use Amp\TimeoutException;
|
||||||
use Amp\WindowsRegistry\KeyNotFoundException;
|
|
||||||
use Amp\WindowsRegistry\WindowsRegistry;
|
|
||||||
use LibDNS\Decoder\DecoderFactory;
|
use LibDNS\Decoder\DecoderFactory;
|
||||||
use LibDNS\Encoder\EncoderFactory;
|
use LibDNS\Encoder\EncoderFactory;
|
||||||
use LibDNS\Messages\MessageFactory;
|
use LibDNS\Messages\MessageFactory;
|
||||||
@ -24,11 +23,15 @@ use function Amp\call;
|
|||||||
class DefaultResolver implements Resolver {
|
class DefaultResolver implements Resolver {
|
||||||
use CallableMaker;
|
use CallableMaker;
|
||||||
|
|
||||||
|
const CACHE_PREFIX = "amphp.dns.";
|
||||||
|
|
||||||
|
private $cache;
|
||||||
|
private $configLoader;
|
||||||
|
private $config;
|
||||||
private $messageFactory;
|
private $messageFactory;
|
||||||
private $questionFactory;
|
private $questionFactory;
|
||||||
private $encoder;
|
private $encoder;
|
||||||
private $decoder;
|
private $decoder;
|
||||||
private $arrayCache;
|
|
||||||
private $requestIdCounter;
|
private $requestIdCounter;
|
||||||
private $pendingRequests;
|
private $pendingRequests;
|
||||||
private $serverIdMap;
|
private $serverIdMap;
|
||||||
@ -36,51 +39,60 @@ class DefaultResolver implements Resolver {
|
|||||||
private $serverIdTimeoutMap;
|
private $serverIdTimeoutMap;
|
||||||
private $now;
|
private $now;
|
||||||
private $serverTimeoutWatcher;
|
private $serverTimeoutWatcher;
|
||||||
private $config;
|
|
||||||
|
|
||||||
public function __construct() {
|
public function __construct(Cache $cache = null, ConfigLoader $configLoader = null) {
|
||||||
|
$this->cache = $cache ?? new ArrayCache;
|
||||||
|
$this->configLoader = $configLoader ?? \stripos(PHP_OS, "win") === 0
|
||||||
|
? new WindowsConfigLoader
|
||||||
|
: new UnixConfigLoader;
|
||||||
|
|
||||||
$this->messageFactory = new MessageFactory;
|
$this->messageFactory = new MessageFactory;
|
||||||
$this->questionFactory = new QuestionFactory;
|
$this->questionFactory = new QuestionFactory;
|
||||||
$this->encoder = (new EncoderFactory)->create();
|
$this->encoder = (new EncoderFactory)->create();
|
||||||
$this->decoder = (new DecoderFactory)->create();
|
$this->decoder = (new DecoderFactory)->create();
|
||||||
$this->arrayCache = new ArrayCache;
|
|
||||||
$this->requestIdCounter = 1;
|
$this->requestIdCounter = 1;
|
||||||
$this->pendingRequests = [];
|
$this->pendingRequests = [];
|
||||||
$this->serverIdMap = [];
|
$this->serverIdMap = [];
|
||||||
$this->serverUriMap = [];
|
$this->serverUriMap = [];
|
||||||
$this->serverIdTimeoutMap = [];
|
$this->serverIdTimeoutMap = [];
|
||||||
$this->now = \time();
|
$this->now = \time();
|
||||||
|
|
||||||
$this->serverTimeoutWatcher = Loop::repeat(1000, function ($watcherId) {
|
$this->serverTimeoutWatcher = Loop::repeat(1000, function ($watcherId) {
|
||||||
$this->now = $now = \time();
|
$this->now = $now = \time();
|
||||||
|
|
||||||
foreach ($this->serverIdTimeoutMap as $id => $expiry) {
|
foreach ($this->serverIdTimeoutMap as $id => $expiry) {
|
||||||
if ($now > $expiry) {
|
if ($now > $expiry) {
|
||||||
$this->unloadServer($id);
|
$this->unloadServer($id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($this->serverIdMap)) {
|
if (empty($this->serverIdMap)) {
|
||||||
Loop::disable($watcherId);
|
Loop::disable($watcherId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Loop::unreference($this->serverTimeoutWatcher);
|
Loop::unreference($this->serverTimeoutWatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @inheritdoc */
|
||||||
* {@inheritdoc}
|
|
||||||
*/
|
|
||||||
public function resolve(string $name, array $options = []): Promise {
|
public function resolve(string $name, array $options = []): Promise {
|
||||||
if (!$inAddr = @\inet_pton($name)) {
|
if (!$inAddr = @\inet_pton($name)) {
|
||||||
if ($this->isValidHostName($name)) {
|
try {
|
||||||
|
$name = normalizeName($name);
|
||||||
$types = empty($options["types"]) ? [Record::A, Record::AAAA] : (array) $options["types"];
|
$types = empty($options["types"]) ? [Record::A, Record::AAAA] : (array) $options["types"];
|
||||||
|
|
||||||
return call(function () use ($name, $types, $options) {
|
return call(function () use ($name, $types, $options) {
|
||||||
$result = yield from $this->recurseWithHosts($name, $types, $options);
|
$result = yield from $this->recurseWithHosts($name, $types, $options);
|
||||||
return $this->flattenResult($result, $types);
|
return $this->flattenResult($result, $types);
|
||||||
});
|
});
|
||||||
} else {
|
} catch (InvalidNameError $e) {
|
||||||
return new Failure(new ResolutionException("Cannot resolve; invalid host name"));
|
return new Failure(new ResolutionException("Cannot resolve invalid host name ({$name})", 0, $e));
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return new Success([[$name, isset($inAddr[4]) ? Record::AAAA : Record::A, $ttl = null]]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// It's already a valid IP, don't resolve, immediately return
|
||||||
|
return new Success([[$name, isset($inAddr[4]) ? Record::AAAA : Record::A, $ttl = null]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -100,9 +112,21 @@ class DefaultResolver implements Resolver {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isValidHostName($name) {
|
private function loadConfig(bool $forceReload = false): Promise {
|
||||||
static $pattern = '/^(?<name>[a-z0-9]([a-z0-9-]*[a-z0-9])?)(\.(?&name))*$/i';
|
if ($this->config && !$forceReload) {
|
||||||
return !isset($name[253]) && \preg_match($pattern, $name);
|
return new Success($this->config);
|
||||||
|
}
|
||||||
|
|
||||||
|
$promise = $this->configLoader->loadConfig();
|
||||||
|
$promise->onResolve(function ($error, $result) {
|
||||||
|
if ($error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->config = $result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return $promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
// flatten $result while preserving order according to $types (append unspecified types for e.g. Record::ALL queries)
|
// flatten $result while preserving order according to $types (append unspecified types for e.g. Record::ALL queries)
|
||||||
@ -122,7 +146,9 @@ class DefaultResolver implements Resolver {
|
|||||||
if (!isset($options["hosts"]) || $options["hosts"]) {
|
if (!isset($options["hosts"]) || $options["hosts"]) {
|
||||||
static $hosts = null;
|
static $hosts = null;
|
||||||
if ($hosts === null || !empty($options["reload_hosts"])) {
|
if ($hosts === null || !empty($options["reload_hosts"])) {
|
||||||
$hosts = yield from $this->loadHostsFile();
|
/** @var Config $config */
|
||||||
|
$config = yield $this->loadConfig(!empty($options["reload_hosts"]));
|
||||||
|
$hosts = $config->getKnownHosts();
|
||||||
}
|
}
|
||||||
$result = [];
|
$result = [];
|
||||||
if (\in_array(Record::A, $types) && isset($hosts[Record::A][$name])) {
|
if (\in_array(Record::A, $types) && isset($hosts[Record::A][$name])) {
|
||||||
@ -216,9 +242,8 @@ class DefaultResolver implements Resolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function doResolve($name, array $types, $options) {
|
private function doResolve($name, array $types, $options) {
|
||||||
if (!$this->config) {
|
/** @var Config $config */
|
||||||
$this->config = yield from $this->loadResolvConf();
|
$config = yield $this->loadConfig();
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($types)) {
|
if (empty($types)) {
|
||||||
return [];
|
return [];
|
||||||
@ -239,36 +264,34 @@ class DefaultResolver implements Resolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$name = \strtolower($name);
|
$name = normalizeName($name);
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
// Check for cache hits
|
// Check for cache hits
|
||||||
if (!isset($options["cache"]) || $options["cache"]) {
|
if (!isset($options["cache"]) || $options["cache"]) {
|
||||||
foreach ($types as $k => $type) {
|
foreach ($types as $k => $type) {
|
||||||
$cacheKey = "$name#$type";
|
$cacheKey = "$name#$type";
|
||||||
$cacheValue = yield $this->arrayCache->get($cacheKey);
|
$cacheValue = yield $this->cache->get(self::CACHE_PREFIX . $cacheKey);
|
||||||
|
|
||||||
if ($cacheValue !== null) {
|
if ($cacheValue !== null) {
|
||||||
$result[$type] = \json_decode($cacheValue, true);
|
$result[$type] = \json_decode($cacheValue, true);
|
||||||
unset($types[$k]);
|
unset($types[$k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (empty($types)) {
|
if (empty($types)) {
|
||||||
if (empty(array_filter($result))) {
|
if (empty(array_filter($result))) {
|
||||||
throw new NoRecordException("No records returned for {$name} (cached result)");
|
throw new NoRecordException("No records returned for {$name} (cached result)");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$timeout = empty($options["timeout"]) ? $this->config["timeout"] : (int) $options["timeout"];
|
$timeout = empty($options["timeout"]) ? $config->getTimeout() : (int) $options["timeout"];
|
||||||
|
|
||||||
if (empty($options["server"])) {
|
if (empty($options["server"])) {
|
||||||
if (empty($this->config["nameservers"])) {
|
$uri = "udp://" . $config->getNameservers()[0];
|
||||||
throw new ResolutionException("No nameserver specified in system config");
|
|
||||||
}
|
|
||||||
|
|
||||||
$uri = "udp://" . $this->config["nameservers"][0];
|
|
||||||
} else {
|
} else {
|
||||||
$uri = $this->parseCustomServerUri($options["server"]);
|
$uri = $this->parseCustomServerUri($options["server"]);
|
||||||
}
|
}
|
||||||
@ -283,12 +306,13 @@ class DefaultResolver implements Resolver {
|
|||||||
foreach ($resultArr as $value) {
|
foreach ($resultArr as $value) {
|
||||||
$result += $value;
|
$result += $value;
|
||||||
}
|
}
|
||||||
} catch (TimeoutException $e) {
|
} catch (Amp\TimeoutException $e) {
|
||||||
if (\substr($uri, 0, 6) == "tcp://") {
|
if (\substr($uri, 0, 6) === "tcp://") {
|
||||||
throw new TimeoutException(
|
throw new TimeoutException(
|
||||||
"Name resolution timed out for {$name}"
|
"Name resolution timed out for {$name}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$options["server"] = \preg_replace("#[a-z.]+://#", "tcp://", $uri);
|
$options["server"] = \preg_replace("#[a-z.]+://#", "tcp://", $uri);
|
||||||
return yield from $this->doResolve($name, $types, $options);
|
return yield from $this->doResolve($name, $types, $options);
|
||||||
} catch (ResolutionException $e) {
|
} catch (ResolutionException $e) {
|
||||||
@ -309,154 +333,6 @@ class DefaultResolver implements Resolver {
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @link http://man7.org/linux/man-pages/man5/resolv.conf.5.html */
|
|
||||||
private function loadResolvConf($path = null) {
|
|
||||||
$result = [
|
|
||||||
"nameservers" => [
|
|
||||||
"8.8.8.8:53",
|
|
||||||
"8.8.4.4:53",
|
|
||||||
],
|
|
||||||
"timeout" => 3000,
|
|
||||||
"attempts" => 2,
|
|
||||||
];
|
|
||||||
|
|
||||||
if (\stripos(PHP_OS, "win") !== 0 || $path !== null) {
|
|
||||||
$path = $path ?: "/etc/resolv.conf";
|
|
||||||
|
|
||||||
try {
|
|
||||||
$lines = \explode("\n", yield \Amp\File\get($path));
|
|
||||||
$result["nameservers"] = [];
|
|
||||||
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
$line = \preg_split('#\s+#', $line, 2);
|
|
||||||
if (\count($line) !== 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
list($type, $value) = $line;
|
|
||||||
if ($type === "nameserver") {
|
|
||||||
$line[1] = \trim($line[1]);
|
|
||||||
$ip = @\inet_pton($line[1]);
|
|
||||||
|
|
||||||
if ($ip === false) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($ip[15])) {
|
|
||||||
$result["nameservers"][] = "[" . $line[1] . "]:53";
|
|
||||||
} else {
|
|
||||||
$result["nameservers"][] = $line[1] . ":53";
|
|
||||||
}
|
|
||||||
} elseif ($type === "options") {
|
|
||||||
$optline = \preg_split('#\s+#', $value, 2);
|
|
||||||
if (\count($optline) !== 2) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Respect the contents of the attempts setting during resolution
|
|
||||||
|
|
||||||
list($option, $value) = $optline;
|
|
||||||
if (\in_array($option, ["timeout", "attempts"])) {
|
|
||||||
$result[$option] = (int) $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (FilesystemException $e) {
|
|
||||||
// use default
|
|
||||||
}
|
|
||||||
} elseif (\stripos(PHP_OS, "win") === 0) {
|
|
||||||
$keys = [
|
|
||||||
"HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\NameServer",
|
|
||||||
"HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\DhcpNameServer",
|
|
||||||
];
|
|
||||||
|
|
||||||
$reader = new WindowsRegistry;
|
|
||||||
$nameserver = "";
|
|
||||||
|
|
||||||
while ($nameserver === "" && ($key = \array_shift($keys))) {
|
|
||||||
try {
|
|
||||||
$nameserver = yield $reader->read($key);
|
|
||||||
} catch (KeyNotFoundException $e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($nameserver === "") {
|
|
||||||
$subKeys = (yield $reader->listKeys("HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces"));
|
|
||||||
|
|
||||||
foreach ($subKeys as $key) {
|
|
||||||
foreach (["NameServer", "DhcpNameServer"] as $property) {
|
|
||||||
try {
|
|
||||||
$nameserver = (yield $reader->read("{$key}\\{$property}"));
|
|
||||||
|
|
||||||
if ($nameserver !== "") {
|
|
||||||
break 2;
|
|
||||||
}
|
|
||||||
} catch (KeyNotFoundException $e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($nameserver !== "") {
|
|
||||||
// Microsoft documents space as delimiter, AppVeyor uses comma.
|
|
||||||
$result["nameservers"] = \array_map(function ($ns) {
|
|
||||||
return \trim($ns) . ":53";
|
|
||||||
}, \explode(" ", \strtr($nameserver, ",", " ")));
|
|
||||||
} else {
|
|
||||||
throw new ResolutionException("Could not find a nameserver in the Windows Registry.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function loadHostsFile($path = null) {
|
|
||||||
$data = [];
|
|
||||||
if (empty($path)) {
|
|
||||||
$path = \stripos(PHP_OS, "win") === 0
|
|
||||||
? 'C:\Windows\system32\drivers\etc\hosts'
|
|
||||||
: '/etc/hosts';
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$contents = yield \Amp\File\get($path);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
$lines = \array_filter(\array_map("trim", \explode("\n", $contents)));
|
|
||||||
foreach ($lines as $line) {
|
|
||||||
if ($line[0] === "#") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$parts = \preg_split('/\s+/', $line);
|
|
||||||
if (!($ip = @\inet_pton($parts[0]))) {
|
|
||||||
continue;
|
|
||||||
} elseif (isset($ip[4])) {
|
|
||||||
$key = Record::AAAA;
|
|
||||||
} else {
|
|
||||||
$key = Record::A;
|
|
||||||
}
|
|
||||||
for ($i = 1, $l = \count($parts); $i < $l; $i++) {
|
|
||||||
if ($this->isValidHostName($parts[$i])) {
|
|
||||||
$data[$key][\strtolower($parts[$i])] = $parts[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Windows does not include localhost in its host file. Fetch it from the system instead
|
|
||||||
if (!isset($data[Record::A]["localhost"]) && !isset($data[Record::AAAA]["localhost"])) {
|
|
||||||
// PHP currently provides no way to **resolve** IPv6 hostnames (not even with fallback)
|
|
||||||
$local = \gethostbyname("localhost");
|
|
||||||
if ($local !== "localhost") {
|
|
||||||
$data[Record::A]["localhost"] = $local;
|
|
||||||
} else {
|
|
||||||
$data[Record::AAAA]["localhost"] = "::1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function parseCustomServerUri($uri) {
|
private function parseCustomServerUri($uri) {
|
||||||
if (!\is_string($uri)) {
|
if (!\is_string($uri)) {
|
||||||
throw new ResolutionException(
|
throw new ResolutionException(
|
||||||
@ -643,7 +519,8 @@ class DefaultResolver implements Resolver {
|
|||||||
$result[$record->getType()][] = [(string) $record->getData(), $record->getType(), $record->getTTL()];
|
$result[$record->getType()][] = [(string) $record->getData(), $record->getType(), $record->getTTL()];
|
||||||
}
|
}
|
||||||
if (empty($result)) {
|
if (empty($result)) {
|
||||||
$this->arrayCache->set("$name#$type", \json_encode([]), 300); // "it MUST NOT cache it for longer than five (5) minutes" per RFC 2308 section 7.1
|
// "it MUST NOT cache it for longer than five (5) minutes" per RFC 2308 section 7.1
|
||||||
|
$this->cache->set(self::CACHE_PREFIX . "$name#$type", \json_encode([]), 300);
|
||||||
$this->finalizeResult($serverId, $requestId, new NoRecordException(
|
$this->finalizeResult($serverId, $requestId, new NoRecordException(
|
||||||
"No records returned for {$name}"
|
"No records returned for {$name}"
|
||||||
));
|
));
|
||||||
@ -678,7 +555,7 @@ class DefaultResolver implements Resolver {
|
|||||||
$minttl = $ttl;
|
$minttl = $ttl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$this->arrayCache->set("$name#$type", \json_encode($records), $minttl);
|
$this->cache->set(self::CACHE_PREFIX . "$name#$type", \json_encode($records), $minttl);
|
||||||
}
|
}
|
||||||
$deferred->resolve($result);
|
$deferred->resolve($result);
|
||||||
}
|
}
|
||||||
|
73
lib/HostLoader.php
Normal file
73
lib/HostLoader.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
use Amp\File;
|
||||||
|
use Amp\Promise;
|
||||||
|
use function Amp\call;
|
||||||
|
|
||||||
|
class HostLoader {
|
||||||
|
private $path;
|
||||||
|
|
||||||
|
public function __construct(string $path = null) {
|
||||||
|
$this->path = $path ?? $this->getDefaultPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getDefaultPath(): string {
|
||||||
|
return \stripos(PHP_OS, "win") === 0
|
||||||
|
? 'C:\Windows\system32\drivers\etc\hosts'
|
||||||
|
: '/etc/hosts';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadHosts(): Promise {
|
||||||
|
return call(function () {
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$contents = yield File\get($this->path);
|
||||||
|
} catch (File\FilesystemException $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = \array_filter(\array_map("trim", \explode("\n", $contents)));
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
if ($line[0] === "#") { // Skip comments
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = \preg_split('/\s+/', $line);
|
||||||
|
|
||||||
|
if (!($ip = @\inet_pton($parts[0]))) {
|
||||||
|
continue;
|
||||||
|
} elseif (isset($ip[4])) {
|
||||||
|
$key = Record::AAAA;
|
||||||
|
} else {
|
||||||
|
$key = Record::A;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ($i = 1, $l = \count($parts); $i < $l; $i++) {
|
||||||
|
try {
|
||||||
|
$normalizedName = normalizeName($parts[$i]);
|
||||||
|
$data[$key][$normalizedName] = $parts[0];
|
||||||
|
} catch (InvalidNameError $e) {
|
||||||
|
// ignore invalid entries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows does not include localhost in its host file. Fetch it from the system instead
|
||||||
|
if (!isset($data[Record::A]["localhost"]) && !isset($data[Record::AAAA]["localhost"])) {
|
||||||
|
// PHP currently provides no way to **resolve** IPv6 hostnames (not even with fallback)
|
||||||
|
$local = \gethostbyname("localhost");
|
||||||
|
if ($local !== "localhost") {
|
||||||
|
$data[Record::A]["localhost"] = $local;
|
||||||
|
} else {
|
||||||
|
$data[Record::AAAA]["localhost"] = "::1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
6
lib/InvalidNameError.php
Normal file
6
lib/InvalidNameError.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
class InvalidNameError extends \Error {
|
||||||
|
}
|
80
lib/UnixConfigLoader.php
Normal file
80
lib/UnixConfigLoader.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
use Amp\File;
|
||||||
|
use Amp\File\FilesystemException;
|
||||||
|
use Amp\Promise;
|
||||||
|
use function Amp\call;
|
||||||
|
|
||||||
|
class UnixConfigLoader implements ConfigLoader {
|
||||||
|
private $path;
|
||||||
|
private $hostLoader;
|
||||||
|
|
||||||
|
public function __construct(string $path = "/etc/resolv.conf", HostLoader $hostLoader = null) {
|
||||||
|
$this->path = $path;
|
||||||
|
$this->hostLoader = $hostLoader ?? new HostLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadConfig(): Promise {
|
||||||
|
return call(function () {
|
||||||
|
$path = $this->path;
|
||||||
|
$nameservers = [];
|
||||||
|
$timeout = 3000;
|
||||||
|
$attempts = 2;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$fileContent = yield File\get($path);
|
||||||
|
$lines = \explode("\n", $fileContent);
|
||||||
|
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = \preg_split('#\s+#', $line, 2);
|
||||||
|
|
||||||
|
if (\count($line) !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($type, $value) = $line;
|
||||||
|
|
||||||
|
if ($type === "nameserver") {
|
||||||
|
$value = \trim($value);
|
||||||
|
$ip = @\inet_pton($value);
|
||||||
|
|
||||||
|
if ($ip === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($ip[15])) { // IPv6
|
||||||
|
$nameservers[] = "[" . $value . "]:53";
|
||||||
|
} else { // IPv4
|
||||||
|
$nameservers[] = $value . ":53";
|
||||||
|
}
|
||||||
|
} elseif ($type === "options") {
|
||||||
|
$optline = \preg_split('#\s+#', $value, 2);
|
||||||
|
|
||||||
|
if (\count($optline) !== 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($option, $value) = $optline;
|
||||||
|
|
||||||
|
switch ($option) {
|
||||||
|
case "timeout":
|
||||||
|
$timeout = (int) $value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "attempts":
|
||||||
|
$attempts = (int) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (FilesystemException $e) {
|
||||||
|
throw new ConfigException("Could not read configuration file ({$path})", $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hosts = yield $this->hostLoader->loadHosts();
|
||||||
|
|
||||||
|
return new Config($nameservers, $hosts, $timeout, $attempts);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
73
lib/WindowsConfigLoader.php
Normal file
73
lib/WindowsConfigLoader.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\Dns;
|
||||||
|
|
||||||
|
use Amp\Promise;
|
||||||
|
use Amp\WindowsRegistry\KeyNotFoundException;
|
||||||
|
use Amp\WindowsRegistry\WindowsRegistry;
|
||||||
|
use function Amp\call;
|
||||||
|
|
||||||
|
class WindowsConfigLoader implements ConfigLoader {
|
||||||
|
public function loadConfig(): Promise {
|
||||||
|
return call(function () {
|
||||||
|
$keys = [
|
||||||
|
"HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\NameServer",
|
||||||
|
"HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\DhcpNameServer",
|
||||||
|
];
|
||||||
|
|
||||||
|
$reader = new WindowsRegistry;
|
||||||
|
$nameserver = "";
|
||||||
|
|
||||||
|
while ($nameserver === "" && ($key = \array_shift($keys))) {
|
||||||
|
try {
|
||||||
|
$nameserver = yield $reader->read($key);
|
||||||
|
} catch (KeyNotFoundException $e) {
|
||||||
|
// retry other possible locations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nameserver === "") {
|
||||||
|
$interfaces = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\Interfaces";
|
||||||
|
$subKeys = yield $reader->listKeys($interfaces);
|
||||||
|
|
||||||
|
foreach ($subKeys as $key) {
|
||||||
|
foreach (["NameServer", "DhcpNameServer"] as $property) {
|
||||||
|
try {
|
||||||
|
$nameserver = yield $reader->read("{$key}\\{$property}");
|
||||||
|
|
||||||
|
if ($nameserver !== "") {
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
} catch (KeyNotFoundException $e) {
|
||||||
|
// retry other possible locations
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nameserver === "") {
|
||||||
|
throw new ConfigException("Could not find a nameserver in the Windows Registry");
|
||||||
|
}
|
||||||
|
|
||||||
|
$nameservers = [];
|
||||||
|
|
||||||
|
// Microsoft documents space as delimiter, AppVeyor uses comma, we just accept both
|
||||||
|
foreach (\explode(" ", \strtr($nameserver, ",", " ")) as $nameserver) {
|
||||||
|
$nameserver = \trim($nameserver);
|
||||||
|
$ip = @\inet_pton($nameserver);
|
||||||
|
|
||||||
|
if ($ip === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($ip[15])) { // IPv6
|
||||||
|
$nameservers[] = "[" . $nameserver . "]:53";
|
||||||
|
} else { // IPv4
|
||||||
|
$nameservers[] = $nameserver . ":53";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Config($nameservers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ const LOOP_STATE_IDENTIFIER = Resolver::class;
|
|||||||
* Retrieve the application-wide dns resolver instance.
|
* Retrieve the application-wide dns resolver instance.
|
||||||
*
|
*
|
||||||
* @param \Amp\Dns\Resolver $resolver Optionally specify a new default dns resolver instance
|
* @param \Amp\Dns\Resolver $resolver Optionally specify a new default dns resolver instance
|
||||||
|
*
|
||||||
* @return \Amp\Dns\Resolver Returns the application-wide dns resolver instance
|
* @return \Amp\Dns\Resolver Returns the application-wide dns resolver instance
|
||||||
*/
|
*/
|
||||||
function resolver(Resolver $resolver = null): Resolver {
|
function resolver(Resolver $resolver = null): Resolver {
|
||||||
@ -58,7 +59,8 @@ function driver(): Resolver {
|
|||||||
* - "reload_hosts" | bool Reload the hosts file (Default: false), only active when no_hosts not true
|
* - "reload_hosts" | bool Reload the hosts file (Default: false), only active when no_hosts not true
|
||||||
* - "cache" | bool Use local DNS cache when querying (Default: true)
|
* - "cache" | bool Use local DNS cache when querying (Default: true)
|
||||||
* - "types" | array Default: [Record::A, Record::AAAA] (only for resolve())
|
* - "types" | array Default: [Record::A, Record::AAAA] (only for resolve())
|
||||||
* - "recurse" | bool Check for DNAME and CNAME records (always active for resolve(), Default: false for query())
|
* - "recurse" | bool Check for DNAME and CNAME records (always active for resolve(), Default: false for
|
||||||
|
* query())
|
||||||
*
|
*
|
||||||
* If the custom per-request "server" option is not present the resolver will
|
* If the custom per-request "server" option is not present the resolver will
|
||||||
* use the first nameserver in /etc/resolv.conf or default to Google's public
|
* use the first nameserver in /etc/resolv.conf or default to Google's public
|
||||||
@ -66,6 +68,7 @@ function driver(): Resolver {
|
|||||||
*
|
*
|
||||||
* @param string $name The hostname to resolve
|
* @param string $name The hostname to resolve
|
||||||
* @param array $options
|
* @param array $options
|
||||||
|
*
|
||||||
* @return \Amp\Promise
|
* @return \Amp\Promise
|
||||||
* @TODO add boolean "clear_cache" option flag
|
* @TODO add boolean "clear_cache" option flag
|
||||||
*/
|
*/
|
||||||
@ -76,15 +79,29 @@ function resolve(string $name, array $options = []): Promise {
|
|||||||
/**
|
/**
|
||||||
* Query specific DNS records.
|
* Query specific DNS records.
|
||||||
*
|
*
|
||||||
* @param string $name Unlike resolve(), query() allows for requesting _any_ name (as DNS RFC allows for arbitrary strings)
|
* @param string $name Unlike resolve(), query() allows for requesting _any_ name (as DNS RFC allows for arbitrary
|
||||||
|
* strings)
|
||||||
* @param int|int[] $type Use constants of Amp\Dns\Record
|
* @param int|int[] $type Use constants of Amp\Dns\Record
|
||||||
* @param array $options @see resolve documentation
|
* @param array $options @see resolve documentation
|
||||||
|
*
|
||||||
* @return \Amp\Promise
|
* @return \Amp\Promise
|
||||||
*/
|
*/
|
||||||
function query(string $name, $type, array $options = []): Promise {
|
function query(string $name, $type, array $options = []): Promise {
|
||||||
return resolver()->query($name, $type, $options);
|
return resolver()->query($name, $type, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a string is a valid DNS name.
|
||||||
|
*
|
||||||
|
* @param string $name DNS name to check.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
function isValidHostName(string $name): bool {
|
||||||
|
static $pattern = '/^(?<name>[a-z0-9]([a-z0-9-]*[a-z0-9])?)(\.(?&name))*$/i';
|
||||||
|
return !isset($name[253]) && \preg_match($pattern, $name);
|
||||||
|
}
|
||||||
|
|
||||||
if (\function_exists('idn_to_ascii')) {
|
if (\function_exists('idn_to_ascii')) {
|
||||||
/**
|
/**
|
||||||
* Normalizes a DNS name.
|
* Normalizes a DNS name.
|
||||||
@ -97,7 +114,7 @@ if (\function_exists('idn_to_ascii')) {
|
|||||||
*/
|
*/
|
||||||
function normalizeName(string $label): string {
|
function normalizeName(string $label): string {
|
||||||
if (false === $result = \idn_to_ascii($label, 0, INTL_IDNA_VARIANT_UTS46)) {
|
if (false === $result = \idn_to_ascii($label, 0, INTL_IDNA_VARIANT_UTS46)) {
|
||||||
throw new \Error("Label '{$label}' could not be processed for IDN");
|
throw new InvalidNameError("Label '{$label}' could not be processed for IDN");
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
@ -105,7 +122,7 @@ if (\function_exists('idn_to_ascii')) {
|
|||||||
} else {
|
} else {
|
||||||
function normalizeName(string $label): string {
|
function normalizeName(string $label): string {
|
||||||
if (\preg_match('/[\x80-\xff]/', $label)) {
|
if (\preg_match('/[\x80-\xff]/', $label)) {
|
||||||
throw new \Error(
|
throw new InvalidNameError(
|
||||||
"Label '{$label}' contains non-ASCII characters and IDN support is not available."
|
"Label '{$label}' contains non-ASCII characters and IDN support is not available."
|
||||||
. " Verify that ext/intl is installed for IDN support."
|
. " Verify that ext/intl is installed for IDN support."
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace Amp\Dns\Test;
|
namespace Amp\Dns\Test;
|
||||||
|
|
||||||
|
use Amp\Dns;
|
||||||
|
use Amp\Dns\Record;
|
||||||
use Amp\Loop;
|
use Amp\Loop;
|
||||||
use Amp\PHPUnit\TestCase;
|
use Amp\PHPUnit\TestCase;
|
||||||
|
|
||||||
@ -12,7 +14,8 @@ class IntegrationTest extends TestCase {
|
|||||||
*/
|
*/
|
||||||
public function testResolve($hostname) {
|
public function testResolve($hostname) {
|
||||||
Loop::run(function () use ($hostname) {
|
Loop::run(function () use ($hostname) {
|
||||||
$result = (yield \Amp\Dns\resolve($hostname));
|
$result = yield Dns\resolve($hostname);
|
||||||
|
|
||||||
list($addr, $type, $ttl) = $result[0];
|
list($addr, $type, $ttl) = $result[0];
|
||||||
$inAddr = @\inet_pton($addr);
|
$inAddr = @\inet_pton($addr);
|
||||||
$this->assertNotFalse(
|
$this->assertNotFalse(
|
||||||
@ -28,10 +31,11 @@ class IntegrationTest extends TestCase {
|
|||||||
*/
|
*/
|
||||||
public function testResolveWithCustomServer($server) {
|
public function testResolveWithCustomServer($server) {
|
||||||
Loop::run(function () use ($server) {
|
Loop::run(function () use ($server) {
|
||||||
$result = (yield \Amp\Dns\resolve("google.com", [
|
$result = yield Dns\resolve("google.com", [
|
||||||
"server" => $server
|
"server" => $server,
|
||||||
]));
|
]);
|
||||||
list($addr, $type, $ttl) = $result[0];
|
|
||||||
|
list($addr) = $result[0];
|
||||||
$inAddr = @\inet_pton($addr);
|
$inAddr = @\inet_pton($addr);
|
||||||
$this->assertNotFalse(
|
$this->assertNotFalse(
|
||||||
$inAddr,
|
$inAddr,
|
||||||
@ -40,12 +44,13 @@ class IntegrationTest extends TestCase {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testPtrLoopup() {
|
public function testPtrLookup() {
|
||||||
Loop::run(function () {
|
Loop::run(function () {
|
||||||
$result = (yield \Amp\Dns\query("8.8.4.4", \Amp\Dns\Record::PTR));
|
$result = yield Dns\query("8.8.4.4", Record::PTR);
|
||||||
|
|
||||||
list($addr, $type) = $result[0];
|
list($addr, $type) = $result[0];
|
||||||
$this->assertSame($addr, "google-public-dns-b.google.com");
|
$this->assertSame("google-public-dns-b.google.com", $addr);
|
||||||
$this->assertSame($type, \Amp\Dns\Record::PTR);
|
$this->assertSame(Record::PTR, $type);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,41 +3,31 @@
|
|||||||
namespace Amp\Dns\Test;
|
namespace Amp\Dns\Test;
|
||||||
|
|
||||||
use Amp\Coroutine;
|
use Amp\Coroutine;
|
||||||
|
use Amp\Dns\Config;
|
||||||
|
use Amp\Dns\ConfigException;
|
||||||
|
use Amp\Dns\UnixConfigLoader;
|
||||||
use Amp\PHPUnit\TestCase;
|
use Amp\PHPUnit\TestCase;
|
||||||
use ReflectionObject;
|
use ReflectionObject;
|
||||||
|
use function Amp\Promise\wait;
|
||||||
|
|
||||||
class ResolvConfTest extends TestCase {
|
class ResolvConfTest extends TestCase {
|
||||||
public function test() {
|
public function test() {
|
||||||
$reflector = new ReflectionObject(\Amp\Dns\resolver());
|
$loader = new UnixConfigLoader(__DIR__ . "/data/resolv.conf");
|
||||||
$method = $reflector->getMethod("loadResolvConf");
|
|
||||||
$method->setAccessible(true);
|
|
||||||
|
|
||||||
$result = \Amp\Promise\wait(new Coroutine($method->invoke(\Amp\Dns\resolver(), __DIR__ . "/data/resolv.conf")));
|
/** @var Config $result */
|
||||||
|
$result = wait($loader->loadConfig());
|
||||||
|
|
||||||
$this->assertSame([
|
$this->assertSame([
|
||||||
"nameservers" => [
|
"127.0.0.1:53",
|
||||||
"127.0.0.1:53",
|
"[2001:4860:4860::8888]:53",
|
||||||
"[2001:4860:4860::8888]:53"
|
], $result->getNameservers());
|
||||||
],
|
|
||||||
"timeout" => 5000,
|
$this->assertSame(5000, $result->getTimeout());
|
||||||
"attempts" => 3,
|
$this->assertSame(3, $result->getAttempts());
|
||||||
], $result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testDefaultsOnConfNotFound() {
|
public function testDefaultsOnConfNotFound() {
|
||||||
$reflector = new ReflectionObject(\Amp\Dns\resolver());
|
$this->expectException(ConfigException::class);
|
||||||
$method = $reflector->getMethod("loadResolvConf");
|
wait((new UnixConfigLoader(__DIR__ . "/data/non-existent.conf"))->loadConfig());
|
||||||
$method->setAccessible(true);
|
|
||||||
|
|
||||||
$result = \Amp\Promise\wait(new Coroutine($method->invoke(\Amp\Dns\resolver(), __DIR__ . "/data/invalid.conf")));
|
|
||||||
|
|
||||||
$this->assertSame([
|
|
||||||
"nameservers" => [
|
|
||||||
"8.8.8.8:53",
|
|
||||||
"8.8.4.4:53"
|
|
||||||
],
|
|
||||||
"timeout" => 3000,
|
|
||||||
"attempts" => 2,
|
|
||||||
], $result);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user