From 48e37f4014fc47bc674f22a5209cfacdb20c8aec Mon Sep 17 00:00:00 2001 From: Niklas Keller Date: Fri, 23 Jun 2017 09:24:26 +0200 Subject: [PATCH] Implement caching and name normalization in BasicResolver --- lib/BasicResolver.php | 106 ++++++++++++++++++++++++++++++++++++------ lib/Resolver.php | 2 +- lib/UdpServer.php | 5 +- 3 files changed, 95 insertions(+), 18 deletions(-) diff --git a/lib/BasicResolver.php b/lib/BasicResolver.php index 2436643..4c63dbc 100644 --- a/lib/BasicResolver.php +++ b/lib/BasicResolver.php @@ -2,12 +2,17 @@ namespace Amp\Dns; +use Amp\Cache\ArrayCache; +use Amp\Cache\Cache; use Amp\Coroutine; use Amp\Promise; +use LibDNS\Messages\MessageTypes; use LibDNS\Records\Question; use LibDNS\Records\QuestionFactory; class BasicResolver implements Resolver { + const CACHE_PREFIX = "amphp.dns."; + /** @var \Amp\Dns\ConfigLoader */ private $configLoader; @@ -17,7 +22,11 @@ class BasicResolver implements Resolver { /** @var \Amp\Dns\Config|null */ private $config; - public function __construct(ConfigLoader $configLoader = null) { + /** @var Cache */ + private $cache; + + 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; @@ -25,14 +34,13 @@ class BasicResolver implements Resolver { $this->questionFactory = new QuestionFactory; } - /** - * @see \Amp\Dns\resolve - */ + /** @inheritdoc */ public function resolve(string $name): Promise { // TODO: Implement resolve() method. } - public function query(string $name, $type): Promise { + /** @inheritdoc */ + public function query(string $name, int $type): Promise { return new Coroutine($this->doQuery($name, $type)); } @@ -41,8 +49,13 @@ class BasicResolver implements Resolver { $this->config = yield $this->configLoader->loadConfig(); } + $name = $this->normalizeName($name, $type); $question = $this->createQuestion($name, $type); + if (null !== $cachedValue = yield $this->cache->get($this->getCacheKey($name, $type))) { + return $this->decodeCachedResult($name, $type, $cachedValue); + } + $nameservers = $this->config->getNameservers(); $attempts = $this->config->getAttempts(); @@ -50,8 +63,6 @@ class BasicResolver implements Resolver { $i = $attempt % \count($nameservers); $uri = "udp://" . $nameservers[$i]; - var_dump($uri); - /** @var \Amp\Dns\Server $server */ $server = yield UdpServer::connect($uri); @@ -59,34 +70,99 @@ class BasicResolver implements Resolver { $response = yield $server->ask($question); if ($response->getResponseCode() !== 0) { - throw new ResolutionException(\sprintf("Got a response code of %d", $response->getResponseCode())); + throw new ResolutionException(\sprintf("Server returned error code: %d", $response->getResponseCode())); + } + + if ($response->getType() !== MessageTypes::RESPONSE) { + throw new ResolutionException("Invalid server reply; expected RESPONSE but received QUERY"); + } + + if ($response->isTruncated()) { + // TODO: Retry via TCP } $answers = $response->getAnswerRecords(); - $result = []; + $ttls = []; + /** @var \LibDNS\Records\Resource $record */ foreach ($answers as $record) { - $result[] = $record; + $recordType = $record->getType(); + + $result[$recordType][] = $record->getData(); + $ttls[$recordType] = \min($ttls[$recordType] ?? \PHP_INT_MAX, $record->getTTL()); } - return $result; + + foreach ($result as $recordType => $records) { + // We don't care here whether storing in the cache fails + $this->cache->set($this->getCacheKey($name, $recordType), \json_encode($records), $ttls[$recordType]); + } + + if (!isset($result[$type])) { + // "it MUST NOT cache it for longer than five (5) minutes" per RFC 2308 section 7.1 + $this->cache->set($this->getCacheKey($name, $type), \json_encode([]), 300); + throw new NoRecordException("No records returned for {$name}"); + } + + return array_map(function ($data) use ($type, $ttls) { + return new Record($data, $type, $ttls[$type]); + }, $result[$type]); } - throw new ResolutionException("No response from any nameserver"); + throw new ResolutionException("No response from any nameserver after {$attempts} attempts"); } /** * @param string $name - * @param int $type + * @param int $type * * @return \LibDNS\Records\Question */ private function createQuestion(string $name, int $type): Question { if (0 > $type || 0xffff < $type) { - throw new \Error(\sprintf('%d does not correspond to a valid record type (must be between 0 and 65535).', $type)); + $message = \sprintf('%d does not correspond to a valid record type (must be between 0 and 65535).', $type); + throw new \Error($message); } + $question = $this->questionFactory->create($type); $question->setName($name); + return $question; } -} \ No newline at end of file + + private function getCacheKey(string $name, int $type): string { + return self::CACHE_PREFIX . $name . "#" . $type; + } + + private function decodeCachedResult(string $name, string $type, string $encoded) { + $decoded = \json_decode($encoded, true); + + if (!$decoded) { + throw new NoRecordException("No records returned for {$name} (cached result)"); + } + + $result = []; + + foreach ($decoded as $data) { + $result[] = new Record($data, $type); + } + + return $result; + } + + private function normalizeName(string $name, int $type) { + if ($type === Record::PTR) { + if (($packedIp = @inet_pton($name)) !== false) { + if (isset($packedIp[4])) { // IPv6 + $name = wordwrap(strrev(bin2hex($packedIp)), 1, ".", true) . ".ip6.arpa"; + } else { // IPv4 + $name = inet_ntop(strrev($packedIp)) . ".in-addr.arpa"; + } + } + } else if (\in_array($type, [Record::A, Record::AAAA])) { + $name = normalizeDnsName($name); + } + + return $name; + } +} diff --git a/lib/Resolver.php b/lib/Resolver.php index 068faf7..a7a5611 100644 --- a/lib/Resolver.php +++ b/lib/Resolver.php @@ -13,5 +13,5 @@ interface Resolver { /** * @see \Amp\Dns\query */ - public function query(string $name, $type): Promise; + public function query(string $name, int $type): Promise; } diff --git a/lib/UdpServer.php b/lib/UdpServer.php index 963ba41..36dffdf 100644 --- a/lib/UdpServer.php +++ b/lib/UdpServer.php @@ -10,8 +10,10 @@ use LibDNS\Messages\Message; use function Amp\call; class UdpServer extends Server { + /** @var \LibDNS\Encoder\Encoder */ private $encoder; + /** @var \LibDNS\Decoder\Decoder */ private $decoder; public static function connect(string $uri): Promise { @@ -27,7 +29,6 @@ class UdpServer extends Server { return new Success(new self($socket)); } - protected function __construct($socket) { parent::__construct($socket); @@ -51,4 +52,4 @@ class UdpServer extends Server { return $this->decoder->decode($data); }); } -} \ No newline at end of file +}