1
0
mirror of https://github.com/danog/dns.git synced 2025-01-22 13:31:12 +01:00

Refactor to option-less interface

This commit is contained in:
Niklas Keller 2017-06-22 23:25:21 +02:00
parent 3c8412b4a7
commit 74e68587f7
6 changed files with 144 additions and 155 deletions

View File

@ -47,7 +47,6 @@
"Amp\\Dns\\": "lib"
},
"files": [
"lib/constants.php",
"lib/functions.php"
]
}

View File

@ -13,6 +13,10 @@ class Config {
throw new ConfigException("At least one nameserver is required for a valid config");
}
foreach ($nameservers as $nameserver) {
$this->validateNameserver($nameserver);
}
if ($timeout < 0) {
throw new ConfigException("Invalid timeout ({$timeout}), must be 0 or greater");
}
@ -27,6 +31,43 @@ class Config {
$this->attempts = $attempts;
}
private function validateNameserver(string $nameserver) {
if (!$nameserver) {
throw new ConfigException("Invalid nameserver: {$nameserver}");
}
if ($nameserver[0] === "[") { // IPv6
$addr = \strstr(\substr($nameserver, 1), "]", true);
$port = \substr($nameserver, \strrpos($nameserver, "]") + 1);
if ($port !== "" && !\preg_match("(^:(\\d+)$)", $port, $match)) {
throw new ConfigException("Invalid nameserver: {$nameserver}");
}
$port = $port === "" ? 53 : \substr($port, 1);
} else { // IPv4
$arr = \explode(":", $nameserver, 2);
if (\count($arr) === 2) {
list($addr, $port) = $arr;
} else {
$addr = $arr[0];
$port = 53;
}
}
$addr = \trim($addr, "[]");
$port = (int) $port;
if (!$inAddr = @\inet_pton($addr)) {
throw new ConfigException("Invalid server IP: {$addr}");
}
if ($port < 1 || $port > 65535) {
throw new ConfigException("Invalid server port: {$port}");
}
}
public function getNameservers(): array {
return $this->nameservers;
}

View File

@ -23,6 +23,8 @@ use function Amp\call;
class DefaultResolver implements Resolver {
use CallableMaker;
const MAX_REQUEST_ID = 65536;
const IDLE_TIMEOUT = 15000;
const CACHE_PREFIX = "amphp.dns.";
private $cache;
@ -83,9 +85,7 @@ class DefaultResolver implements Resolver {
return call(function () use ($name) {
$result = yield from $this->recurseWithHosts($name, [Record::A, Record::AAAA], []);
return array_map(function ($record) {
return new Record($record[0], $record[1], $record[2]);
}, $this->flattenResult($result, [Record::A, Record::AAAA]));
return $this->flattenResult($result, [Record::A, Record::AAAA]);
});
} catch (InvalidNameError $e) {
return new Failure(new ResolutionException("Cannot resolve invalid host name ({$name})", 0, $e));
@ -97,22 +97,19 @@ class DefaultResolver implements Resolver {
}
/** @inheritdoc */
public function query(string $name, $type, array $options = []): Promise {
public function query(string $name, $type): Promise {
$types = (array) $type;
return call(function () use ($name, $types, $options) {
if (empty($options["recurse"])) {
$result = yield from $this->doResolve($name, $types, $options);
} else {
$result = yield from $this->doRecurse($name, $types, $options);
}
return array_map(function ($record) {
return new Record($record[0], $record[1], $record[2]);
}, $this->flattenResult($result, $types));
return call(function () use ($name, $types) {
$result = yield from $this->doResolve($name, $types);
return $this->flattenResult($result, $types);
});
}
public function reloadConfig(): Promise {
return $this->loadConfig(true);
}
private function loadConfig(bool $forceReload = false): Promise {
if ($this->config && !$forceReload) {
return new Success($this->config);
@ -131,54 +128,60 @@ class DefaultResolver implements Resolver {
}
// flatten $result while preserving order according to $types (append unspecified types for e.g. Record::ALL queries)
private function flattenResult(array $result, array $types) {
private function flattenResult(array $result, array $types): array {
$retval = [];
foreach ($types as $type) {
if (isset($result[$type])) {
$retval = \array_merge($retval, $result[$type]);
unset($result[$type]);
}
}
return $result ? \array_merge($retval, \call_user_func_array("array_merge", $result)) : $retval;
$records = $result ? \array_merge($retval, \call_user_func_array("array_merge", $result)) : $retval;
return array_map(function ($record) {
return new Record($record[0], $record[1], $record[2]);
}, $records);
}
private function recurseWithHosts($name, array $types, $options) {
// Check for hosts file matches
if (!isset($options["hosts"]) || $options["hosts"]) {
static $hosts = null;
if ($hosts === null || !empty($options["reload_hosts"])) {
/** @var Config $config */
$config = yield $this->loadConfig(!empty($options["reload_hosts"]));
$hosts = $config->getKnownHosts();
}
$result = [];
if (\in_array(Record::A, $types) && isset($hosts[Record::A][$name])) {
$result[Record::A] = [[$hosts[Record::A][$name], Record::A, $ttl = null]];
}
if (\in_array(Record::AAAA, $types) && isset($hosts[Record::AAAA][$name])) {
$result[Record::AAAA] = [[$hosts[Record::AAAA][$name], Record::AAAA, $ttl = null]];
}
if ($result) {
return $result;
}
private function recurseWithHosts($name, array $types) {
/** @var Config $config */
$config = yield $this->loadConfig();
$hosts = $config->getKnownHosts();
$result = [];
if (\in_array(Record::A, $types) && isset($hosts[Record::A][$name])) {
$result[Record::A] = [[$hosts[Record::A][$name], Record::A, $ttl = null]];
}
return yield from $this->doRecurse($name, $types, $options);
if (\in_array(Record::AAAA, $types) && isset($hosts[Record::AAAA][$name])) {
$result[Record::AAAA] = [[$hosts[Record::AAAA][$name], Record::AAAA, $ttl = null]];
}
if ($result) {
return $result;
}
return yield from $this->doRecurse($name, $types);
}
private function doRecurse($name, array $types, $options) {
private function doRecurse($name, array $types) {
if (\array_intersect($types, [Record::CNAME, Record::DNAME])) {
throw new ResolutionException("Cannot use recursion for CNAME and DNAME records");
throw new \Error("Cannot use recursion for CNAME and DNAME records");
}
$types = \array_merge($types, [Record::CNAME, Record::DNAME]);
$lookupName = $name;
for ($i = 0; $i < 30; $i++) {
$result = yield from $this->doResolve($lookupName, $types, $options);
$result = yield from $this->doResolve($lookupName, $types);
if (\count($result) > isset($result[Record::CNAME]) + isset($result[Record::DNAME])) {
unset($result[Record::CNAME], $result[Record::DNAME]);
return $result;
}
// @TODO check for potentially using recursion and iterate over *all* CNAME/DNAME
// @FIXME check higher level for CNAME?
foreach ([Record::CNAME, Record::DNAME] as $type) {
@ -193,8 +196,8 @@ class DefaultResolver implements Resolver {
private function doRequest($uri, $name, $type) {
$server = $this->loadExistingServer($uri) ?: $this->loadNewServer($uri);
$useTCP = \substr($uri, 0, 6) == "tcp://";
if ($useTCP && isset($server->connect)) {
return call(function () use ($server, $uri, $name, $type) {
yield $server->connect;
@ -205,7 +208,7 @@ class DefaultResolver implements Resolver {
// Get the next available request ID
do {
$requestId = $this->requestIdCounter++;
if ($this->requestIdCounter >= MAX_REQUEST_ID) {
if ($this->requestIdCounter >= self::MAX_REQUEST_ID) {
$this->requestIdCounter = 1;
}
} while (isset($this->pendingRequests[$requestId]));
@ -228,6 +231,7 @@ class DefaultResolver implements Resolver {
}
// Send request
// FIXME: Fix might not write all bytes if TCP is used, as the buffer might be full
$bytesWritten = @\fwrite($server->socket, $requestPacket);
if ($bytesWritten === false || $bytesWritten === 0 && (!\is_resource($server->socket) || !\feof($server->socket))) {
$exception = new ResolutionException("Request send failed");
@ -242,7 +246,7 @@ class DefaultResolver implements Resolver {
return $deferred->promise();
}
private function doResolve($name, array $types, $options) {
private function doResolve($name, array $types) {
/** @var Config $config */
$config = yield $this->loadConfig();
@ -269,95 +273,63 @@ class DefaultResolver implements Resolver {
$result = [];
// Check for cache hits
if (!isset($options["cache"]) || $options["cache"]) {
foreach ($types as $k => $type) {
$cacheKey = "$name#$type";
$cacheValue = yield $this->cache->get(self::CACHE_PREFIX . $cacheKey);
foreach ($types as $k => $type) {
$cacheValue = yield $this->cache->get($this->getCacheKey($name, $type));
if ($cacheValue !== null) {
$result[$type] = \json_decode($cacheValue, true);
unset($types[$k]);
if ($cacheValue !== null) {
$result[$type] = \json_decode($cacheValue, true);
unset($types[$k]);
}
}
if (empty($types)) {
// TODO: Why do we use array_filter here?
if (empty(array_filter($result))) {
throw new NoRecordException("No records returned for {$name} (cached result)");
}
return $result;
}
$nameservers = $config->getNameservers();
$attempts = $config->getAttempts();
for ($attempt = 0; $attempt < $attempts; $attempt++) {
$i = $attempt % \count($nameservers);
$uri = "udp://" . $nameservers[$i];
$promises = [];
foreach ($types as $type) {
$promises[] = $this->doRequest($uri, $name, $type);
}
try {
list(, $resultArr) = yield Promise\timeout(Promise\some($promises), $config->getTimeout());
foreach ($resultArr as $value) {
$result += $value;
}
}
if (empty($types)) {
if (empty(array_filter($result))) {
throw new NoRecordException("No records returned for {$name} (cached result)");
} catch (Amp\TimeoutException $e) {
continue;
} catch (ResolutionException $e) {
if (empty($result)) { // if we have no cached results
throw $e;
}
return $result;
}
}
$timeout = empty($options["timeout"]) ? $config->getTimeout() : (int) $options["timeout"];
if (empty($options["server"])) {
$uri = "udp://" . $config->getNameservers()[0];
} else {
$uri = $this->parseCustomServerUri($options["server"]);
}
$promises = [];
foreach ($types as $type) {
$promises[] = $this->doRequest($uri, $name, $type);
}
try {
list(, $resultArr) = yield Promise\timeout(Promise\some($promises), $timeout);
foreach ($resultArr as $value) {
$result += $value;
}
} catch (Amp\TimeoutException $e) {
if (\substr($uri, 0, 6) === "tcp://") {
throw new TimeoutException(
"Name resolution timed out for {$name}"
);
}
$options["server"] = \preg_replace("#[a-z.]+://#", "tcp://", $uri);
return yield from $this->doResolve($name, $types, $options);
} catch (ResolutionException $e) {
if (empty($result)) { // if we have no cached results
throw $e;
}
} catch (MultiReasonException $e) { // if all promises in Amp\some fail
if (empty($result)) { // if we have no cached results
foreach ($e->getReasons() as $ex) {
if ($ex instanceof NoRecordException) {
throw new NoRecordException("No records returned for {$name}", 0, $e);
} catch (MultiReasonException $e) { // if all promises in Amp\some fail
if (empty($result)) { // if we have no cached results
foreach ($e->getReasons() as $ex) {
if ($ex instanceof NoRecordException) {
throw new NoRecordException("No records returned for {$name}", 0, $e);
}
}
throw new ResolutionException("All name resolution requests failed", 0, $e);
}
throw new ResolutionException("All name resolution requests failed", 0, $e);
}
return $result;
}
return $result;
}
private function parseCustomServerUri($uri) {
if (!\is_string($uri)) {
throw new ResolutionException(
'Invalid server address ($uri must be a string IP address, ' . \gettype($uri) . " given)"
);
}
if (\strpos($uri, "://") !== false) {
return $uri;
}
if (($colonPos = \strrpos($uri, ":")) !== false) {
$addr = \substr($uri, 0, $colonPos);
$port = \substr($uri, $colonPos + 1);
} else {
$addr = $uri;
$port = 53;
}
$addr = \trim($addr, "[]");
if (!$inAddr = @\inet_pton($addr)) {
throw new ResolutionException(
'Invalid server $uri; string IP address required'
);
}
return isset($inAddr[4]) ? "udp://[{$addr}]:{$port}" : "udp://{$addr}:{$port}";
throw $e;
}
private function loadExistingServer($uri) {
@ -410,6 +382,7 @@ class DefaultResolver implements Resolver {
unset($server->connect);
$deferred->resolve();
});
// TODO: Respect timeout
$timer = Loop::delay(5000, function () use ($id, $deferred, $watcher, $uri) {
Loop::cancel($watcher);
$this->unloadServer($id);
@ -521,7 +494,7 @@ class DefaultResolver implements Resolver {
}
if (empty($result)) {
// "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->cache->set($this->getCacheKey($name, $type), \json_encode([]), 300);
$this->finalizeResult($serverId, $requestId, new NoRecordException(
"No records returned for {$name}"
));
@ -542,7 +515,7 @@ class DefaultResolver implements Resolver {
$server->pendingRequests[$requestId]
);
if (empty($server->pendingRequests)) {
$this->serverIdTimeoutMap[$server->id] = $this->now + IDLE_TIMEOUT;
$this->serverIdTimeoutMap[$server->id] = $this->now + self::IDLE_TIMEOUT;
Loop::disable($server->watcherId);
Loop::enable($this->serverTimeoutWatcher);
}
@ -561,4 +534,8 @@ class DefaultResolver implements Resolver {
$deferred->resolve($result);
}
}
private function getCacheKey(string $name, int $type): string {
return self::CACHE_PREFIX . $name . "#" . $type;
}
}

View File

@ -13,5 +13,5 @@ interface Resolver {
/**
* @see \Amp\Dns\query
*/
public function query(string $name, $type, array $options = []): Promise;
public function query(string $name, $type): Promise;
}

View File

@ -1,8 +0,0 @@
<?php
namespace Amp\Dns;
// @codeCoverageIgnoreStart
const MAX_REQUEST_ID = 65536;
const IDLE_TIMEOUT = 15000;
// @codeCoverageIgnoreEnd

View File

@ -26,26 +26,6 @@ class IntegrationTest extends TestCase {
});
}
/**
* @group internet
* @dataProvider provideServers
*/
public function testResolveWithCustomServer($server) {
Loop::run(function () use ($server) {
$result = yield Dns\resolve("google.com", [
"server" => $server,
]);
/** @var Record $record */
$record = $result[0];
$inAddr = @\inet_pton($record->getValue());
$this->assertNotFalse(
$inAddr,
"Server name google.com did not resolve to a valid IP address via $server"
);
});
}
public function testPtrLookup() {
Loop::run(function () {
$result = yield Dns\query("8.8.4.4", Record::PTR);