From a2fe7a5764d163070ea94dff8e78ada35b467010 Mon Sep 17 00:00:00 2001 From: Daniel Lowrey Date: Sat, 1 Aug 2015 22:18:44 -0400 Subject: [PATCH] Massive refactor using amp/1.0.0 --- .coveralls.yml | 1 + .editorconfig | 16 + .gitattributes | 9 + .gitignore | 7 +- .php_cs | 15 + .travis.yml | 16 +- CONTRIBUTING.md | 30 ++ LICENSE | 21 ++ README.md | 100 ++---- composer.json | 26 +- examples/001_sync_wait.php | 9 - examples/002_async.php | 34 -- lib/AddressModes.php | 12 - lib/Client.php | 382 ---------------------- lib/HostsFile.php | 120 ------- lib/NoRecordException.php | 5 + lib/RequestBuilder.php | 66 ---- lib/ResolutionErrors.php | 10 - lib/Resolver.php | 77 ----- lib/ResponseInterpreter.php | 83 ----- lib/TimeoutException.php | 5 + lib/constants.php | 14 + lib/functions.php | 480 ++++++++++++++++++++++------ phpunit.xml | 46 +-- test/ClientTest.php | 102 ------ test/HostsFileTest.php | 133 -------- test/IntegrationTest.php | 192 ++--------- test/ResolverTest.php | 80 ----- test/ResponseInterpreterTest.php | 300 ----------------- test/bootstrap.php | 4 +- test/fixtures/ipv4Hosts.txt | 2 - test/fixtures/ipv6Hosts.txt | 3 - test/fixtures/mixedVersionHosts.txt | 14 - test/fixtures/resolverTest.txt | 8 - 34 files changed, 601 insertions(+), 1821 deletions(-) create mode 100644 .coveralls.yml create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .php_cs create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE delete mode 100644 examples/001_sync_wait.php delete mode 100644 examples/002_async.php delete mode 100644 lib/AddressModes.php delete mode 100644 lib/Client.php delete mode 100644 lib/HostsFile.php create mode 100644 lib/NoRecordException.php delete mode 100644 lib/RequestBuilder.php delete mode 100644 lib/ResolutionErrors.php delete mode 100644 lib/Resolver.php delete mode 100644 lib/ResponseInterpreter.php create mode 100644 lib/TimeoutException.php create mode 100644 lib/constants.php delete mode 100644 test/ClientTest.php delete mode 100644 test/HostsFileTest.php delete mode 100644 test/ResolverTest.php delete mode 100644 test/ResponseInterpreterTest.php delete mode 100644 test/fixtures/ipv4Hosts.txt delete mode 100644 test/fixtures/ipv6Hosts.txt delete mode 100644 test/fixtures/mixedVersionHosts.txt delete mode 100644 test/fixtures/resolverTest.txt diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 0000000..6b74c21 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +src_dir: lib diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..47cfede --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = spaces +charset = utf-8 + +[{.travis.yml}] +indent_style = space +indent_size = 2 + +[{composer.json}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c521702 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +/test export-ignore +/examples export-ignore +/.coveralls.yml export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php_cs export-ignore +/.travis.yml export-ignore +/phpunit.xml export-ignore diff --git a/.gitignore b/.gitignore index 4f38912..f31187e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -.idea -vendor -composer.lock +/coverage/ +/composer.lock +/vendor/ +/.idea/ diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..ab38568 --- /dev/null +++ b/.php_cs @@ -0,0 +1,15 @@ +level(Symfony\CS\FixerInterface::NONE_LEVEL) + ->fixers([ + "psr2", + "-braces", + "-psr0", + ]) + ->finder( + Symfony\CS\Finder\DefaultFinder::create() + ->in(__DIR__ . "/lib") + ->in(__DIR__ . "/test") + ) +; diff --git a/.travis.yml b/.travis.yml index 636fd2c..6ccbfc4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,19 @@ +sudo: false + language: php -git: - submodules: false - php: - - 5.6 - 5.5 + - 5.6 + - 7 before_script: + - composer self-update - composer install -script: php ./vendor/bin/phpunit --configuration ./phpunit.xml --coverage-text +script: + - vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml + - php vendor/bin/php-cs-fixer --diff --dry-run -v fix + +after_script: + - php vendor/bin/coveralls -v diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..cee345a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +## Submitting useful bug reports + +Please search existing issues first to make sure this is not a duplicate. +Every issue report has a cost for the developers required to field it; be +respectful of others' time and ensure your report isn't spurious prior to +submission. Please adhere to [sound bug reporting principles](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html). + +## Development ideology + +Truths which we believe to be self-evident: + +- **It's an asynchronous world.** Be wary of anything that undermines + async principles. + +- **The answer is not more options.** If you feel compelled to expose + new preferences to the user it's very possible you've made a wrong + turn somewhere. + +- **There are no power users.** The idea that some users "understand" + concepts better than others has proven to be, for the most part, false. + If anything, "power users" are more dangerous than the rest, and we + should avoid exposing dangerous functionality to them. + +## Code style + +The amphp project adheres to the PSR-2 style guide with the exception that +opening braces for classes and methods must appear on the same line as +the declaration: + +https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..87a8244 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 amphp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 8e9e59a..bf2e71a 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,41 @@ -dns [![Build Status](https://travis-ci.org/amphp/dns.svg?branch=master)](https://travis-ci.org/amphp/dns) -============ +# dns -Asynchronous DNS resolution built on the [Amp](https://github.com/amphp/amp) concurrency framework +[![Build Status](https://img.shields.io/travis/amphp/dns/master.svg?style=flat-square)](https://travis-ci.org/amphp/dns) +[![CoverageStatus](https://img.shields.io/coveralls/amphp/dns/master.svg?style=flat-square)](https://coveralls.io/github/amphp/dns?branch=master) +![Unstable](https://img.shields.io/badge/api-unstable-orange.svg?style=flat-square) +![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square) -## Examples +`amphp/dns` provides asynchronous DNS name resolution based on the [`amp`](https://github.com/amphp/amp) +concurrency framework. -**Synchronous Resolution Via `Amp\wait()`** +**Required PHP Version** -```php -resolve($name)); -printf("%s resolved to %s\n", $name, $address); +**Installation** + +```bash +$ composer require amphp/dns:dev-master ``` -**Concurrent Synchronous Resolution Via `wait()`** +**Example** ```php resolve($name); -} -$comboPromise = Amp\all($promises); -$results = Amp\wait($comboPromise); -foreach ($results as $name => $resultArray) { - list($addr, $type) = $resultArray; - printf("%s => %s\n", $name, $addr); -} -``` +Amp\run(function () { + $githubIpv4 = (yield Amp\Dns\resolve("github.com")); + var_dump($githubIpv4); + $googleIpv4 = Amp\Dns\resolve("google.com"); + $googleIpv6 = Amp\Dns\resolve("google.com", $options = [ + "mode" => Amp\Dns\MODE_INET6 + ]); -**Event Loop Async** - -```php -resolve($name); - $promises[$name] = $promise; - } - - // Yield control until the combo promise resolves - list($errors, $successes) = (yield 'some' => $promises); - - foreach ($names as $name) { - echo isset($errors[$name]) - ? "FAILED: {$name}\n" - : "{$name} => {$successes[$name][0]}\n"; - } - - // Stop the event loop so we don't sit around forever - Amp\stop(); + $firstGoogleResult = (yield Amp\first([$ipv4Result, $ipv6Result])); + var_dump($firstGoogleResult); }); ``` - -## Tests - -[![Build Status](https://travis-ci.org/amphp/dns.svg?branch=master)](https://travis-ci.org/amphp/dns) - -Tests can be run from the command line using: - -`php vendor/bin/phpunit -c phpunit.xml` - -or to exclude tests that require a working internet connection: - -`php vendor/bin/phpunit -c phpunit.xml --exclude-group internet` diff --git a/composer.json b/composer.json index d3d3cbf..143b546 100644 --- a/composer.json +++ b/composer.json @@ -1,36 +1,38 @@ { "name": "amphp/dns", "homepage": "https://github.com/amphp/dns", - "description": "Asynchronous DNS resolution built on the Amp concurrency framework", + "description": "Async DNS resolution built on the amp concurrency framework", "keywords": ["dns", "resolve", "client", "async", "amp"], "license": "MIT", "authors": [ { "name": "Chris Wright", - "email": "addr@daverandom.com", - "role": "Creator / Lead Developer" + "email": "addr@daverandom.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" } ], "require": { "php": ">=5.5", "amphp/amp": "^1", "amphp/cache": "dev-master", + "amphp/filesystem": "dev-master", "daverandom/libdns": "~1.0" }, "require-dev": { - "mockery/mockery": ">=0.9.1", - "phpunit/phpunit": "~4.1.0" + "phpunit/phpunit": "~4.4.0", + "fabpot/php-cs-fixer": "~1.9", + "satooshi/php-coveralls": "dev-master" }, "autoload": { "psr-4": { "Amp\\Dns\\": "lib" }, - "files": ["lib/functions.php"] - }, - "autoload-dev": { - "psr-4": { - "Amp\\Dns\\Test\\": "test/" - }, - "files": ["test/bootstrap.php"] + "files": [ + "lib/constants.php", + "lib/functions.php" + ] } } diff --git a/examples/001_sync_wait.php b/examples/001_sync_wait.php deleted file mode 100644 index 7bef86f..0000000 --- a/examples/001_sync_wait.php +++ /dev/null @@ -1,9 +0,0 @@ -resolve($name); -list($address, $type) = $promise->wait(); -printf("%s resolved to %s\n", $name, $address); diff --git a/examples/002_async.php b/examples/002_async.php deleted file mode 100644 index 2f8d32a..0000000 --- a/examples/002_async.php +++ /dev/null @@ -1,34 +0,0 @@ -resolve($name); - $promises[$name] = $promise; - } - - // Combine our multiple promises into a single promise - $comboPromise = Amp\some($promises); - - // Yield control until the combo promise resolves - list($errors, $successes) = (yield $comboPromise); - - foreach ($names as $name) { - echo isset($errors[$name]) ? "FAILED: {$name}\n" : "{$name} => {$successes[$name][0]}\n"; - } - - // Stop the event loop so we don't sit around forever - Amp\stop(); -}); diff --git a/lib/AddressModes.php b/lib/AddressModes.php deleted file mode 100644 index 1bde560..0000000 --- a/lib/AddressModes.php +++ /dev/null @@ -1,12 +0,0 @@ -reactor = $reactor ?: \Amp\reactor(); - $this->requestBuilder = $requestBuilder ?: new RequestBuilder; - $this->responseInterpreter = $responseInterpreter ?: new ResponseInterpreter; - $this->cache = $cache ?: (new CacheFactory)->select(); - } - - /** - * Resolve a name from a DNS server - * - * @param string $name - * @param int $mode - * @return \Amp\Promise - */ - public function resolve($name, $mode) { - // Defer UDP server connect until needed to allow custom address/port option assignment - // after object instantiation. - if (empty($this->socket) && !$this->connect()) { - return new Failure(new ResolutionException( - sprintf( - "Failed connecting to DNS server at %s:%d", - $this->serverAddress, - $this->serverPort - ) - )); - } - - if (!$this->isReadWatcherEnabled) { - $this->isReadWatcherEnabled = true; - $this->reactor->enable($this->readWatcherId); - } - - $promisor = new Deferred; - $id = $this->getNextFreeLookupId(); - $this->pendingLookups[$id] = [ - 'name' => $name, - 'requests' => $this->getRequestList($mode), - 'last_type' => null, - 'future' => $promisor, - ]; - - $this->processPendingLookup($id); - - return $promisor->promise(); - } - - private function connect() { - $address = sprintf('udp://%s:%d', $this->serverAddress, $this->serverPort); - if (!$this->socket = @stream_socket_client($address, $errNo, $errStr)) { - return false; - } - - stream_set_blocking($this->socket, 0); - - $this->readWatcherId = $this->reactor->onReadable($this->socket, function() { - $this->onReadableSocket(); - }, ["enable" => false]); - - return true; - } - - private function getNextFreeLookupId() { - do { - $result = $this->lookupIdCounter++; - - if ($this->lookupIdCounter >= PHP_INT_MAX) { - $this->lookupIdCounter = 0; - } - } while(isset($this->pendingLookups[$result])); - - return $result; - } - - 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; - } - - private function getNextFreeRequestId() { - do { - $result = $this->requestIdCounter++; - - if ($this->requestIdCounter >= 65536) { - $this->requestIdCounter = 0; - } - } while (isset($this->pendingRequestsById[$result])); - - return $result; - } - - private function sendRequest($request) { - $packet = $this->requestBuilder->buildRequest($request['id'], $request['name'], $request['type']); - - $bytesWritten = fwrite($this->socket, $packet); - if ($bytesWritten < strlen($packet)) { - $this->completeRequest($request, null, ResolutionErrors::ERR_REQUEST_SEND_FAILED); - return; - } - - $request['timeout_id'] = $this->reactor->once(function() use($request) { - unset($this->pendingRequestsByNameAndType[$request['name']][$request['type']]); - $this->completeRequest($request, null, ResolutionErrors::ERR_SERVER_TIMEOUT); - }, $this->msRequestTimeout); - - $this->pendingRequestsById[$request['id']] = $request; - $this->pendingRequestsByNameAndType[$request['name']][$request['type']] = &$this->pendingRequestsById[$request['id']]; - } - - private function onReadableSocket() { - $packet = fread($this->socket, 512); - - // Decode the response and clean up the pending requests list - $decoded = $this->responseInterpreter->decode($packet); - if ($decoded === null) { - return; - } - - list($id, $response) = $decoded; - $request = $this->pendingRequestsById[$id]; - $name = $request['name']; - - $this->reactor->cancel($request['timeout_id']); - unset( - $this->pendingRequestsById[$id], - $this->pendingRequestsByNameAndType[$name][$request['type']] - ); - - // Interpret the response and make sure we have at least one resource record - $interpreted = $this->responseInterpreter->interpret($response, $request['type']); - if ($interpreted === null) { - foreach ($request['lookups'] as $id => $lookup) { - $this->processPendingLookup($id); - } - - return; - } - - // Distribute the result to the appropriate lookup routine - list($type, $addr, $ttl) = $interpreted; - if ($type === AddressModes::CNAME) { - foreach ($request['lookups'] as $id => $lookup) { - $this->redirectPendingLookup($id, $addr); - } - } else if ($addr !== null) { - $this->cache->set($key, $addr, $ttl); - $this->completeRequest($request, $addr, $type); - } else { - foreach ($request['lookups'] as $id => $lookup) { - $this->processPendingLookup($id); - } - } - } - - private function completePendingLookup($id, $addr, $type) { - if (!isset($this->pendingLookups[$id])) { - return; - } - - $lookupStruct = $this->pendingLookups[$id]; - $future = $lookupStruct['future']; - unset($this->pendingLookups[$id]); - - if ($addr) { - $future->succeed([$addr, $type]); - } else { - $future->fail(new ResolutionException( - $msg = sprintf('DNS resolution failed: %s', $lookupStruct['name']), - $code = $type - )); - } - - if (empty($this->pendingLookups)) { - $this->isReadWatcherEnabled = false; - $this->reactor->disable($this->readWatcherId); - } - } - - private function completeRequest($request, $addr, $type) { - foreach ($request['lookups'] as $id => $lookup) { - $this->completePendingLookup($id, $addr, $type); - } - } - - private function processPendingLookup($id) { - if (!$this->pendingLookups[$id]['requests']) { - $this->completePendingLookup($id, null, ResolutionErrors::ERR_NO_RECORD); - return; - } - - $name = $this->pendingLookups[$id]['name']; - $type = array_shift($this->pendingLookups[$id]['requests']); - $key = $name . $type; - - if (yield $this->cache->has($key)) { - $this->completePendingLookup($id, $addr, $type); - } else { - $this->dispatchRequest($id, $name, $type); - } - } - - private function dispatchRequest($id, $name, $type) { - $this->pendingLookups[$id]['last_type'] = $type; - $this->pendingRequestsByNameAndType[$name][$type]['lookups'][$id] = $this->pendingLookups[$id]; - - if (count($this->pendingRequestsByNameAndType[$name][$type]) === 1) { - $request = [ - 'id' => $this->getNextFreeRequestId(), - 'name' => $name, - 'type' => $type, - 'lookups' => [$id => $this->pendingLookups[$id]], - 'timeout_id' => null, - ]; - - $this->sendRequest($request); - } - } - - private function redirectPendingLookup($id, $name) { - array_unshift($this->pendingLookups[$id]['requests'], $this->pendingLookups[$id]['last_type']); - $this->pendingLookups[$id]['last_type'] = null; - $this->pendingLookups[$id]['name'] = $name; - - $this->processPendingLookup($id); - } - - /** - * Set the Client options - * - * @param int $option - * @param mixed $value - * @throws \RuntimeException If modifying server address/port once connected - * @throws \DomainException On unknown option key - * @return self - */ - public function setOption($option, $value) { - switch ($option) { - case self::OP_MS_REQUEST_TIMEOUT: - $this->msRequestTimeout = (int) $value; - break; - case self::OP_SERVER_ADDRESS: - if ($this->socket) { - throw new \RuntimeException( - 'Server address cannot be modified once connected' - ); - } else { - $this->serverAddress = $value; - } - break; - case self::OP_SERVER_PORT: - if ($this->socket) { - throw new \RuntimeException( - 'Server port cannot be modified once connected' - ); - } else { - $this->serverPort = $value; - } - break; - default: - throw new \DomainException( - sprintf("Unkown option: %s", $option) - ); - } - - return $this; - } -} diff --git a/lib/HostsFile.php b/lib/HostsFile.php deleted file mode 100644 index 16237c4..0000000 --- a/lib/HostsFile.php +++ /dev/null @@ -1,120 +0,0 @@ -path = $path; - } - - private function reload() { - $this->data = [ - AddressModes::INET4_ADDR => [], - AddressModes::INET6_ADDR => [], - ]; - $key = null; - - foreach (file($this->path) as $line) { - $line = trim($line); - if ($line !== '' && $line[0] === '#') { - continue; - } - - $parts = preg_split('/\s+/', $line); - if (!($ip = @inet_pton($parts[0]))) { - continue; - } elseif (isset($ip[4])) { - $key = AddressModes::INET6_ADDR; - } else { - $key = AddressModes::INET4_ADDR; - } - - for ($i = 1, $l = count($parts); $i < $l; $i++) { - if (validateHostName($parts[$i])) { - $this->data[$key][$parts[$i]] = $parts[0]; - } - } - } - } - - private function ensureDataIsCurrent() { - clearstatcache(true, $this->path); - $modTime = filemtime($this->path); - - if ($modTime > $this->mtime) { - $this->reload(); - $this->mtime = $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]); - $want4 = (bool)($mode & AddressModes::INET4_ADDR); - $want6 = (bool)($mode & AddressModes::INET6_ADDR); - $pref6 = (bool)($mode & AddressModes::PREFER_INET6); - - if ($have6 && $want6 && (!$want4 || !$have4 || $pref6)) { - return [$this->data[AddressModes::INET6_ADDR][$name], AddressModes::INET6_ADDR]; - } else if ($have4 && $want4) { - return [$this->data[AddressModes::INET4_ADDR][$name], AddressModes::INET4_ADDR]; - } - - return null; - } - - public function getPath() { - return $this->path; - } -} diff --git a/lib/NoRecordException.php b/lib/NoRecordException.php new file mode 100644 index 0000000..e20426e --- /dev/null +++ b/lib/NoRecordException.php @@ -0,0 +1,5 @@ +messageFactory = $messageFactory ?: new MessageFactory; - $this->questionFactory = $questionFactory ?: new QuestionFactory; - $this->encoder = $encoder ?: (new EncoderFactory)->create(); - } - - /** - * 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); - } -} diff --git a/lib/ResolutionErrors.php b/lib/ResolutionErrors.php deleted file mode 100644 index e7080df..0000000 --- a/lib/ResolutionErrors.php +++ /dev/null @@ -1,10 +0,0 @@ -client = $client ?: new Client; - $this->nameValidator = $nameValidator ?: new NameValidator; - $this->hostsFile = $hostsFile ?: new HostsFile($this->nameValidator); - } - - /** - * Resolve a host name to an IP address - * - * @param string $name - * @param int $mode - * @return \Amp\Promise - */ - public function resolve($name, $mode = AddressModes::ANY_PREFER_INET4) { - if (strcasecmp($name, 'localhost') === 0) { - return new Success($this->resolveLocalhost($mode)); - } elseif ($addrStruct = $this->resolveFromIp($name)) { - return new Success($addrStruct); - } elseif (!$this->nameValidator->validate($name)) { - return new Failure(new ResolutionException( - sprintf('Invalid DNS name format: %s', $name) - )); - } elseif ($this->hostsFile && ($addrStruct = $this->hostsFile->resolve($name, $mode))) { - return new Success($addrStruct); - } else { - return $this->client->resolve($name, $mode); - } - } - - private function resolveLocalhost($mode) { - return ($mode & AddressModes::PREFER_INET6) - ? ['::1', AddressModes::INET6_ADDR] - : ['127.0.0.1', AddressModes::INET4_ADDR]; - } - - private function resolveFromIp($name) { - if (!$inAddr = @inet_pton($name)) { - return []; - } elseif (isset($inAddr['15'])) { - return [$name, AddressModes::INET6_ADDR]; - } else { - return [$name, AddressModes::INET4_ADDR]; - } - } -} diff --git a/lib/ResponseInterpreter.php b/lib/ResponseInterpreter.php deleted file mode 100644 index ceae003..0000000 --- a/lib/ResponseInterpreter.php +++ /dev/null @@ -1,83 +0,0 @@ -decoder = $decoder ?: (new DecoderFactory)->create(); - } - - /** - * Attempt to decode a data packet to a DNS response message - * - * @param string $packet - * @return Message|null - */ - public function decode($packet) { - try { - $message = $this->decoder->decode($packet); - } catch (\Exception $e) { - return null; - } - - if ($message->getType() !== MessageTypes::RESPONSE || $message->getResponseCode() !== 0) { - return null; - } - - return [$message->getID(), $message]; - } - - /** - * Extract the message ID and response data from a DNS response packet - * - * @param Message $message - * @param int $expectedType - * @return array|null - */ - public function interpret($message, $expectedType) { - static $typeMap = [ - AddressModes::INET4_ADDR => ResourceTypes::A, - AddressModes::INET6_ADDR => ResourceTypes::AAAA, - ]; - - $answers = $message->getAnswerRecords(); - if (!count($answers)) { - return null; - } - - /** @var \LibDNS\Records\Resource $record */ - $cname = null; - foreach ($answers as $record) { - switch ($record->getType()) { - case $typeMap[$expectedType]: - return [$expectedType, (string)$record->getData(), $record->getTTL()]; - - case ResourceTypes::CNAME: - $cname = (string)$record->getData(); - break; - } - } - - if ($cname) { - return [AddressModes::CNAME, $cname, null]; - } - - return null; - } -} diff --git a/lib/TimeoutException.php b/lib/TimeoutException.php new file mode 100644 index 0000000..a013a8a --- /dev/null +++ b/lib/TimeoutException.php @@ -0,0 +1,5 @@ +arrayCache->has($cacheKey)) { + $result = (yield $state->arrayCache->get($cacheKey)); + yield new \Amp\CoroutineResult([$result, $mode, $ttl = null]); + return; } - $addr = \inet_ntop($inAddr); - if (isset($inAddr[15])) { - $addr = "[{$addr}]"; - } - $port = (int) $port; - $this->addr = "udp://{$addr}:{$port}"; - } -} - -function initServer() - -function resolver(Reactor $reactor - -class Resolver { - private $ -} - -function resolve($name, array $options = []) { - $generator = __doResolve($name, $options); - - return \Amp\resolve($generator); -} - -function __doResolve($name, $options) { - static $lookupIdCounter; - static $cache; - - if (empty($cache)) { - $cache = new \Amp\Cache\ArrayCache; } - $mode = isset($options["mode"]) ? $options["mode"] : AddressModes::ANY_PREFER_INET4; - if ($mode & AddressModes::PREFER_INET6) { - if ($mode & AddressModes::INET6_ADDR) { - $requests[] = AddressModes::INET6_ADDR; + // Check for hosts file matches + if (empty($options["no_hosts"])) { + $have4 = isset($state->hostsFile[MODE_INET4][$name]); + $have6 = isset($state->hostsFile[MODE_INET6][$name]); + $want4 = (bool)($mode & MODE_INET4); + $want6 = (bool)($mode & MODE_INET6); + if ($have6 && $want6) { + $result = [$state->hostsFile[MODE_INET6][$name], MODE_INET6, $ttl = null]; + } elseif ($have4 && $want4) { + $result = [$state->hostsFile[MODE_INET4][$name], MODE_INET4, $ttl = null]; + } else { + $result = null; } - if ($mode & AddressModes::INET4_ADDR) { - $requests[] = AddressModes::INET4_ADDR; + if ($result) { + yield new \Amp\CoroutineResult($result); + return; } + } + + $timeout = empty($options["timeout"]) ? DEFAULT_TIMEOUT : (int) $options["timeout"]; + + $uri = empty($options["server"]) + ? "udp://" . DEFAULT_SERVER . ":" . DEFAULT_PORT + : __parseCustomServerUri($options["server"]) + ; + $server = __loadExistingServer($state, $uri) ?: __loadNewServer($state, $uri); + + // Get the next available request ID + do { + $requestId = $state->requestIdCounter++; + if ($state->requestIdCounter >= MAX_REQUEST_ID) { + $state->requestIdCounter = 1; + } + } while (isset($state->pendingRequests[$requestId])); + + // Create question record + $questionType = ($mode === MODE_INET4) ? ResourceQTypes::A : ResourceQTypes::AAAA; + $question = $state->questionFactory->create($questionType); + $question->setName($name); + + // Create request message + $request = $state->messageFactory->create(MessageTypes::QUERY); + $request->getQuestionRecords()->add($question); + $request->isRecursionDesired(true); + $request->setID($requestId); + + // Encode request message + $requestPacket = $state->encoder->encode($request); + + // Send request + $bytesWritten = \fwrite($server->socket, $requestPacket); + if ($bytesWritten === false || isset($packet[$bytesWritten])) { + throw new ResolutionException( + "Request send failed" + ); + } + + $promisor = new \Amp\Deferred; + $server->pendingRequests[$requestId] = true; + $state->pendingRequests[$requestId] = [$promisor, $name, $mode]; + + try { + $resultArr = (yield \Amp\timeout($promisor->promise(), $timeout)); + } catch (\Amp\TimeoutException $e) { + throw new TimeoutException( + "Name resolution timed out for {$name}" + ); + } + + list($resultIp, $resultMode, $resultTtl) = $resultArr; + + if ($resultMode === MODE_CNAME) { + $result = (yield resolve($resultIp, $mode, $options)); + list($resultIp, $resultMode, $resultTtl) = $result; + } + + yield $state->arrayCache->set($cacheKey, $resultIp, $resultTtl); + yield new \Amp\CoroutineResult($resultArr); +} + +function __init() { + $state = new \StdClass; + $state->messageFactory = new MessageFactory; + $state->questionFactory = new QuestionFactory; + $state->encoder = (new EncoderFactory)->create(); + $state->decoder = (new DecoderFactory)->create(); + $state->arrayCache = new \Amp\Cache\ArrayCache; + $state->hostsFile = (yield \Amp\resolve(__loadHostsFile())); + $state->requestIdCounter = 1; + $state->pendingRequests = []; + $state->serverIdMap = []; + $state->serverUriMap = []; + $state->serverIdTimeoutMap = []; + $state->now = \time(); + $state->serverTimeoutWatcher = \Amp\repeat(function ($watcherId) use ($state) { + $state->now = $now = \time(); + foreach ($state->serverIdTimeoutMap as $id => $expiry) { + if ($now > $expiry) { + __unloadServer($state, $id); + } + } + if (empty($state->serverIdMap)) { + \Amp\disable($watcherId); + } + }, 1000, $options = [ + "enable" => true, + "keep_alive" => false, + ]); + + yield new \Amp\CoroutineResult($state); +} + +function __loadHostsFile($path = null) { + $data = [ + MODE_INET4 => [], + MODE_INET6 => [], + ]; + if (empty($path)) { + $path = \stripos(PHP_OS, "win") === 0 + ? "C:\\Windows\\system32\\drivers\\etc\\hosts" + : "/etc/hosts" + ; + } + try { + $contents = (yield \Amp\Filesystem\get($path)); + $key = null; + $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 = MODE_INET6; + } else { + $key = MODE_INET4; + } + for ($i = 1, $l = \count($parts); $i < $l; $i++) { + if (__isValidHostName($parts[$i])) { + $data[$key][$parts[$i]] = $parts[0]; + } + } + } + } catch (\Exception $e) { + // hosts file doesn't exist + } + + yield new \Amp\CoroutineResult($data); +} + +function __parseCustomServerUri($uri) { + if (!\is_string($uri)) { + throw new ResolutionException( + "Invalid server address (". gettype($uri) ."); string IP required" + ); + } + if (($colonPos = strrpos(":", $uri)) !== false) { + $addr = \substr($uri, 0, $colonPos); + $port = \substr($uri, $colonPos); } else { - if ($mode & AddressModes::INET4_ADDR) { - $requests[] = AddressModes::INET4_ADDR; - } - if ($mode & AddressModes::INET6_ADDR) { - $requests[] = AddressModes::INET6_ADDR; - } + $addr = $uri; + $port = DEFAULT_PORT; + } + $addr = trim($addr, "[]"); + if (!$inAddr = @\inet_pton($addr)) { + throw new ResolutionException( + "Invalid server URI; IP address required" + ); } - $type = array_shift($requests); - $cacheKey = $name . $type; + return isset($inAddr[15]) ? "udp://[{$addr}]:{$port}" : "udp://{$addr}:{$port}"; +} - if (yield $cache->has($cacheKey)) { - $result = yield $cache->get($cacheKey); - yield new \Amp\CoroutineResult($result); +function __loadExistingServer($state, $uri) { + if (empty($state->serverUriMap[$uri])) { return; } + $server = $state->serverUriMap[$uri]; + if (\is_resource($server->socket) && !@\feof($server->socket)) { + unset($state->serverIdTimeoutMap[$server->id]); + \Amp\enable($server->watcherId); + return $server; + } + __unloadServer($state, $server->id); +} - do { - $lookupId = $lookupIdCounter++; - if ($lookupIdCounter >= PHP_INT_MAX) { - $lookupIdCounter = 0; - } - } while (isset($pendingLookups[$lookupId])); - - $remote = isset($options["server_addr"]) - ? "udp://" . $options["server_addr"] - : "udp://8.8.8.8:53" - ; - - if (!$socket = @\stream_socket_client($remote, $errno, $errstr)) { - throw new ConnectException(sprintf( +function __loadNewServer($state, $uri) { + if (!$socket = @\stream_socket_client($uri, $errno, $errstr)) { + throw new ResolutionException(sprintf( "Connection to %s failed: [Error #%d] %s", $uri, $errno, $errstr )); } + \stream_set_blocking($socket, false); + $id = (int) $socket; + $server = new \StdClass; + $server->id = $id; + $server->uri = $uri; + $server->socket = $socket; + $server->pendingRequests = []; + $server->watcherId = \Amp\onReadable($socket, "Amp\Dns\__onReadable", [ + "enable" => true, + "keep_alive" => true, + "cb_data" => $state, + ]); + $state->serverIdMap[$id] = $server; + $state->serverUriMap[$uri] = $server; - - - if (yield $this->cache->has($cacheKey)) { - $this->completePendingLookup($id, $addr, $type); - } else { - $this->dispatchRequest($id, $name, $type); - } - - $promisor = new Deferred; - $reactor->onReadable($socket, "Amp\Dns\__onReadable", $promisor); - yield $promisor->promise(); - - - - + return $server; } -function __getRequestModesFrom \ No newline at end of file +function __unloadServer($state, $serverId, $error = null) { + $server = $state->serverIdMap[$serverId]; + \Amp\cancel($server->watcherId); + unset( + $state->serverIdMap[$serverId], + $state->serverUriMap[$server->uri] + ); + if (\is_resource($server->socket)) { + @\fclose($server->socket); + } + if ($error && $server->pendingRequests) { + foreach (array_keys($server->pendingRequests) as $requestId) { + list($promisor) = $state->pendingRequests[$requestId]; + $promisor->fail($error); + } + } +} + +function __onReadable($watcherId, $socket, $state) { + $serverId = (int) $socket; + $packet = @\fread($socket, 512); + if ($packet != "") { + __decodeResponsePacket($state, $serverId, $packet); + } else { + __unloadServer($state, $serverId, new ResolutionException( + "Server connection failed" + )); + } +} + +function __decodeResponsePacket($state, $serverId, $packet) { + try { + $response = $state->decoder->decode($packet); + $requestId = $response->getID(); + $responseCode = $response->getResponseCode(); + $responseType = $response->getType(); + + if ($responseCode !== 0) { + __finalizeResult($state, $serverId, $requestId, new ResolutionException( + "Server returned error code: {$responseCode}" + )); + } elseif ($responseType !== MessageTypes::RESPONSE) { + __unloadServer($state, $serverId, new ResolutionException( + "Invalid server reply; expected RESPONSE but received QUERY" + )); + } else { + __processDecodedResponse($state, $serverId, $requestId, $response); + } + } catch (\Exception $e) { + __unloadServer($state, $serverId, new ResolutionException( + "Response decode error", + 0, + $e + )); + } +} + +function __processDecodedResponse($state, $serverId, $requestId, $response) { + static $typeMap = [ + MODE_INET4 => ResourceTypes::A, + MODE_INET6 => ResourceTypes::AAAA, + ]; + + list($promisor, $name, $mode) = $state->pendingRequests[$requestId]; + $answers = $response->getAnswerRecords(); + foreach ($answers as $record) { + switch ($record->getType()) { + case $typeMap[$mode]: + $result = [(string) $record->getData(), $mode, $record->getTTL()]; + break 2; + case ResourceTypes::CNAME: + // CNAME should only be used if no A records exist so we only + // break out of the switch (and not the foreach loop) here. + $result = [(string) $record->getData(), MODE_CNAME, $record->getTTL()]; + break; + } + } + if (empty($result)) { + $recordType = ($mode === MODE_INET4) ? "A" : "AAAA"; + __finalizeResult($state, $serverId, $requestId, new NoRecordException( + "No {$recordType} records returned for {$name}" + )); + } else { + __finalizeResult($state, $serverId, $requestId, $error = null, $result); + } +} + +function __finalizeResult($state, $serverId, $requestId, $error = null, $result = null) { + if (empty($state->pendingRequests[$requestId])) { + return; + } + + list($promisor) = $state->pendingRequests[$requestId]; + $server = $state->serverIdMap[$serverId]; + unset( + $state->pendingRequests[$requestId], + $server->pendingRequests[$requestId] + ); + if (empty($server->pendingRequests)) { + $state->serverIdTimeoutMap[$server->id] = $state->now + IDLE_TIMEOUT; + \Amp\disable($server->watcherId); + \Amp\enable($state->serverTimeoutWatcher); + } + if ($error) { + $promisor->fail($error); + } else { + $promisor->succeed($result); + } +} diff --git a/phpunit.xml b/phpunit.xml index f4097a1..744a96f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,45 +1,13 @@ - - - - - - SQL - - - - - + + + ./test - ./vendor - - + + + + ./lib - - - - ./test - - - - - - - - - - - - - - - - - - - diff --git a/test/ClientTest.php b/test/ClientTest.php deleted file mode 100644 index ff0eb80..0000000 --- a/test/ClientTest.php +++ /dev/null @@ -1,102 +0,0 @@ -setOption(Client::OP_SERVER_ADDRESS, '260.260.260.260'); - $promise = $client->resolve('example.com', AddressModes::INET4_ADDR); - $result = \Amp\wait($promise, $reactor); - } - - /** - * This is just for coverage - which is not worthwhile in itself, - * but it makes it easier to detect missing important coverage. - */ - public function testSetRequestTime() { - $reactor = new NativeReactor; - $client = new Client($reactor); - $client->setOption(Client::OP_MS_REQUEST_TIMEOUT, 1000); - $client->setOption(Client::OP_SERVER_PORT, 53); - } - - /** - * @expectedException \DomainException - */ - public function testUnknownOptionThrowsException() { - $reactor = new NativeReactor; - $client = new Client($reactor); - $client->setOption('foo', 1000); - } - - /** - * @expectedException \RuntimeException - * @group internet - */ - public function testSetAddressAfterConnectException() { - $reactor = new NativeReactor; - $client = new Client($reactor); - $promise = $client->resolve('google.com', AddressModes::INET4_ADDR); - $result = \Amp\wait($promise, $reactor); - $client->setOption(Client::OP_SERVER_ADDRESS, '260.260.260.260'); - } - - /** - * @expectedException \RuntimeException - * @group internet - */ - public function testSetPortAfterConnectException() { - $reactor = new NativeReactor; - $client = new Client($reactor); - $promise = $client->resolve('google.com', AddressModes::INET4_ADDR); - $result = \Amp\wait($promise, $reactor); - $client->setOption(Client::OP_SERVER_PORT, 53); - } - - - /** - * @expectedException \Amp\Dns\ResolutionException - */ - public function testNoAnswers() { - $reactor = new NativeReactor; - $client = new Client($reactor); - $promise = $client->resolve('googleaiusdisuhdihas.apsidjpasjdisjdajsoidaugiug.com', AddressModes::INET4_ADDR); - $result = \Amp\wait($promise, $reactor); - } - - /** - * Test that the overflow of lookupIdCounter and requestIdCounter to - * zero occurs. - */ - public function testPendingIdOverflow() { - $reactor = new NativeReactor; - $class = new \ReflectionClass('Amp\Dns\Client'); - $lookupIdProperty = $class->getProperty("lookupIdCounter"); - $requestIdCounterProperty = $class->getProperty("requestIdCounter"); - - /** @var Client $client */ - $client = $class->newInstance($reactor); - $lookupIdProperty->setAccessible(true); - $lookupIdProperty->setValue($client, PHP_INT_MAX); - $requestIdCounterProperty->setAccessible(true); - $requestIdCounterProperty->setValue($client, 65535); - - $promise = $client->resolve('google.com', AddressModes::INET4_ADDR); - $result = \Amp\wait($promise, $reactor); - $lookupIdCounter = $lookupIdProperty->getValue($client); - $this->assertEquals(0, $lookupIdCounter); - - $requestIdCounter = $lookupIdProperty->getValue($client); - $this->assertEquals(0, $requestIdCounter); - } -} diff --git a/test/HostsFileTest.php b/test/HostsFileTest.php deleted file mode 100644 index c6cf3e7..0000000 --- a/test/HostsFileTest.php +++ /dev/null @@ -1,133 +0,0 @@ -runHostsFileTests($tests, __DIR__ . '/fixtures/ipv4Hosts.txt'); - } - - public function testBasicIPV6() { - $tests = [ - //Examples taken from http://en.wikipedia.org/wiki/IPv6_address - ['2001:db8::2:1', 'host1.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR], - ['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR], - ['2001:db8::1:0:0:1', 'host3.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR], - - //Check that ipv6 is returned when both v4 and v6 are is set - ['2001:db8::2:1', 'host1.example.com', Mode::INET6_ADDR | Mode::INET4_ADDR, Mode::INET6_ADDR], - ['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::INET6_ADDR | Mode::INET4_ADDR, Mode::INET6_ADDR], - ['2001:db8::1:0:0:1', 'host3.example.com', Mode::INET6_ADDR | Mode::INET4_ADDR, Mode::INET6_ADDR], - - //Check that ipv6 is returned when ANY_* is set - ['2001:db8::2:1', 'host1.example.com', Mode::ANY_PREFER_INET4, Mode::INET6_ADDR], - ['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::ANY_PREFER_INET4, Mode::INET6_ADDR], - ['2001:db8::1:0:0:1', 'host3.example.com', Mode::ANY_PREFER_INET4, Mode::INET6_ADDR], - ['2001:db8::2:1', 'host1.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR], - ['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR], - ['2001:db8::1:0:0:1', 'host3.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR], - - //Check request for ipv4 returns null - [null, 'host1.example.com', Mode::INET4_ADDR, null], - - //Check non-existant domains return null - [null, 'host4.example.com', Mode::INET6_ADDR, null], - ]; - - $this->runHostsFileTests($tests, __DIR__ . '/fixtures/ipv6Hosts.txt'); - } - - public function testMixedIPVersions() { - $tests = [ - //Examples taken from http://en.wikipedia.org/wiki/IPv6_address - ['2001:db8::2:1', 'host1.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR], - ['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR], - ['2001:db8::1:0:0:1', 'host3.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR], - ['192.168.1.1', 'host1.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR], - ['192.168.1.2', 'host2.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR], - ['192.168.1.4', 'host4.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR], - - //Check that v4 is returned by default - ['192.168.1.1', 'host1.example.com', Mode::INET4_ADDR | Mode::INET6_ADDR, Mode::INET4_ADDR], - - //Check that the prefer inet6 works - ['2001:db8::2:1', 'host1.example.com', Mode::INET4_ADDR | Mode::INET6_ADDR | Mode::PREFER_INET6, Mode::INET6_ADDR], - - //Check that the ANY_* works - ['192.168.1.1', 'host1.example.com', Mode::ANY_PREFER_INET4, Mode::INET4_ADDR], - ['2001:db8::2:1', 'host1.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR], - - //Check that a host that is only listed as ipv4 does not return a result for ipv6 - [null, 'host4.example.com', Mode::INET6_ADDR, null], - - //Check that a host that is only listed as ipv6 does not return a result for ipv4 - [null, 'host3.example.com', Mode::INET4_ADDR, null], - ]; - - $this->runHostsFileTests($tests, __DIR__ . '/fixtures/mixedVersionHosts.txt'); - } - - public function runHostsFileTests($tests, $hostsFile) { - $nameValidator = new NameValidator; - $hostsFile = new HostsFile($nameValidator, $hostsFile); - - foreach ($tests as $i => $test) { - list($expectedResult, $hostname, $inputAddrMode, $expectedAddrMode) = $test; - - $result = $hostsFile->resolve($hostname, $inputAddrMode); - - if ($expectedResult === null) { - $this->assertNull($result); - } else { - list($resolvedAddr, $resolvedMode) = $result; - - $this->assertEquals( - $expectedAddrMode, - $resolvedMode, - "Failed to resolve $hostname to $expectedResult. " . - "Expected `" . var_export($expectedAddrMode, true) . "` " . - "but got `" . var_export($resolvedAddr, true) . "` " . - " when running test #" . $i - ); - - $this->assertEquals( - $expectedResult, - $resolvedAddr, - "Failed to resolve $hostname to $expectedResult. " . - "Expected `$expectedResult` but got `$resolvedAddr` " . - " when running test #" . $i - ); - } - } - } -} diff --git a/test/IntegrationTest.php b/test/IntegrationTest.php index a832585..04c400d 100644 --- a/test/IntegrationTest.php +++ b/test/IntegrationTest.php @@ -2,170 +2,46 @@ namespace Amp\Dns\Test; -use Amp\NativeReactor; -use Amp\Dns\Cache; -use Amp\Dns\Client; -use Amp\Dns\Resolver; -use Amp\Dns\Cache\APCCache; -use Amp\Dns\Cache\MemoryCache; -use Amp\Dns\Cache\RedisCache; -use Predis\Client as RedisClient; -use Predis\Connection\ConnectionException as RedisConnectionException; - class IntegrationTest extends \PHPUnit_Framework_TestCase { - private static $redisEnabled = true; - private static $redisParameters = [ - 'connection_timeout' => 2, - 'read_write_timeout' => 2, - ]; - - public static function setUpBeforeClass() { - try { - $predisClient = new RedisClient(self::$redisParameters, []); - $predisClient->ping(); - //It's connected - } catch (RedisConnectionException $rce) { - self::$redisEnabled = false; - } - } - - public function testWithNullCache() { - $this->basicRun(null); - } - - public function testWithMemoryCache() { - $memoryCache = new MemoryCache(); - $this->basicRun($memoryCache); - } - - /** - * @requires extension APC - */ - public function testWithApcCache() { - $prefix = time().uniqid('CacheTest'); - $apcCache = new APCCache($prefix); - $this->basicRun($apcCache); - } - - public function testWithRedisCache() { - if (self::$redisEnabled != true) { - $this->markTestSkipped("Could not connect to Redis, skipping test."); - return; - } - - $prefix = time().'_'.uniqid('CacheTest'); - try { - $redisClient = new RedisClient(self::$redisParameters, []); - } - catch (RedisConnectionException $rce) { - $this->markTestIncomplete("Could not connect to Redis server, cannot test redis cache."); - return; - } - - $redisCache = new RedisCache($redisClient, $prefix); - $this->basicRun($redisCache); + protected function setUp() { + \Amp\reactor(\Amp\driver()); } /** * @group internet */ - public function basicRun(Cache $cache = null) { - $names = [ - 'google.com', - 'github.com', - 'stackoverflow.com', - 'localhost', - '192.168.0.1', - '::1', - ]; + public function testResolve() { + \Amp\run(function () { + $names = [ + "google.com", + "github.com", + "stackoverflow.com", + "localhost", + "192.168.0.1", + "::1", + ]; - $reactor = new NativeReactor; - $client = new Client($reactor, null, null, $cache); - $resolver = new Resolver($client); - - $promises = []; - foreach ($names as $name) { - $promises[$name] = $resolver->resolve($name); - } - - $comboPromise = \Amp\all($promises); - $results = \Amp\wait($comboPromise, $reactor); - - foreach ($results as $name => $addrStruct) { - list($addr, $type) = $addrStruct; - $validIP = @inet_pton($addr); - $this->assertNotFalse( - $validIP, - "Server name $name did not resolve to a valid IP address" - ); - } - } - - /** - * Check that caches do actually cache results. - */ - function testCachingOfResults() { - $memoryCache = new MemoryCache(); - - $namesFirstRun = [ - 'google.com', - 'github.com', - 'google.com', - 'github.com', - ]; - - $namesSecondRun = [ - 'google.com', - 'github.com', - ]; - - $setCount = count(array_unique(array_merge($namesFirstRun, $namesSecondRun))); - $getCount = count($namesFirstRun) + count($namesSecondRun); - - $mockedCache = \Mockery::mock($memoryCache); - - /** @var $mockedCache \Mockery\Mock */ - - $mockedCache->shouldReceive('store')->times($setCount)->passthru(); - $mockedCache->shouldReceive('get')->times($getCount)->passthru(); - - $mockedCache->makePartial(); - - $reactor = new NativeReactor; - $client = new Client($reactor, null, null, $mockedCache); - $resolver = new Resolver($client); - - $promises = []; - foreach ($namesFirstRun as $name) { - $promises[$name] = $resolver->resolve($name); - } - - $comboPromise = \Amp\all($promises); - $results = \Amp\wait($comboPromise, $reactor); - - foreach ($results as $name => $addrStruct) { - list($addr, $type) = $addrStruct; - $validIP = @inet_pton($addr); - $this->assertNotFalse( - $validIP, - "Server name $name did not resolve to a valid IP address" - ); - } - - $promises = []; - foreach ($namesSecondRun as $name) { - $promises[$name] = $resolver->resolve($name); - } - - $comboPromise = \Amp\all($promises); - $results = \Amp\wait($comboPromise, $reactor); - foreach ($results as $name => $addrStruct) { - list($addr, $type) = $addrStruct; - $validIP = @inet_pton($addr); - $this->assertNotFalse( - $validIP, - "Server name $name did not resolve to a valid IP address" - ); - } + foreach ($names as $name) { + list($addr, $mode) = (yield \Amp\Dns\resolve($name)); + $inAddr = @\inet_pton($addr); + $this->assertNotFalse( + $inAddr, + "Server name $name did not resolve to a valid IP address" + ); + if (isset($inAddr[15])) { + $this->assertSame( + \Amp\Dns\MODE_INET6, + $mode, + "Returned mode parameter did not match expected MODE_INET6" + ); + } else { + $this->assertSame( + \Amp\Dns\MODE_INET4, + $mode, + "Returned mode parameter did not match expected MODE_INET4" + ); + } + } + }); } } diff --git a/test/ResolverTest.php b/test/ResolverTest.php deleted file mode 100644 index 206e3c1..0000000 --- a/test/ResolverTest.php +++ /dev/null @@ -1,80 +0,0 @@ -createResolver(); - - $alphabet = implode(range('a', 'z')); - $tooLongName = $alphabet.$alphabet; //52 - $tooLongName = $tooLongName.$tooLongName; //104 - $tooLongName = $tooLongName.$tooLongName; //208 - $tooLongName = $tooLongName.$alphabet; //234 - $tooLongName = $tooLongName.$alphabet; //260 - - $promise = $resolver->resolve($tooLongName, AddressModes::PREFER_INET6); - $addrStruct = \Amp\wait($promise, $reactor); - } - - /** - * @group internet - * @expectedException Amp\Dns\ResolutionException - * @expectedErrorCode Amp\Dns\ResolutionErrors::ERR_NO_RECORD - */ - public function testUnknownName() { - list($reactor, $resolver) = $this->createResolver(); - $promise = $resolver->resolve("doesntexist", AddressModes::PREFER_INET6); - $addrStruct = \Amp\wait($promise, $reactor); - } - - public function testLocalHostResolution() { - list($reactor, $resolver) = $this->createResolver(); - - $promise = $resolver->resolve("localhost", AddressModes::INET4_ADDR); - list($addr, $type) = \Amp\wait($promise, $reactor); - $this->assertSame('127.0.0.1', $addr); - $this->assertSame(AddressModes::INET4_ADDR, $type, "Wrong result type - should be INET4_ADDR but got $type"); - - $promise = $resolver->resolve("localhost", AddressModes::PREFER_INET6); - list($addr, $type) = \Amp\wait($promise, $reactor); - $this->assertSame('::1', $addr); - $this->assertSame(AddressModes::INET6_ADDR, $type, "Wrong result type - should be INET6_ADDR but got $type"); - } - - public function testHostsFileResolution() { - $hostsFile = __DIR__ . '/fixtures/resolverTest.txt'; - list($reactor, $resolver) = $this->createResolver($hostsFile); - - $promise = $resolver->resolve("host1.example.com", AddressModes::INET4_ADDR); - list($addr, $type) = \Amp\wait($promise, $reactor); - $this->assertSame('192.168.1.1', $addr); - $this->assertSame(AddressModes::INET4_ADDR, $type); - - $promise = $resolver->resolve("resolvertest", AddressModes::INET4_ADDR); - list($addr, $type) = \Amp\wait($promise, $reactor); - $this->assertSame('192.168.1.3', $addr); - $this->assertSame(AddressModes::INET4_ADDR, $type); - } -} diff --git a/test/ResponseInterpreterTest.php b/test/ResponseInterpreterTest.php deleted file mode 100644 index 1f4efef..0000000 --- a/test/ResponseInterpreterTest.php +++ /dev/null @@ -1,300 +0,0 @@ -shouldReceive('decode')->withAnyArgs()->andThrow("Exception", "Testing bad packet"); - $responseInterpreter = new ResponseInterpreter($decoder); - $result = $responseInterpreter->decode("SomePacket"); - $this->assertNull($result); - } - - public function testInvalidMessage() { - $message = \Mockery::mock('LibDNS\Messages\Message'); - $message->shouldReceive('getType')->once()->andReturn(\LibDNS\Messages\MessageTypes::QUERY); - - $decoder = \Mockery::mock('LibDNS\Decoder\Decoder'); - $decoder->shouldReceive('decode')->once()->andReturn($message); - - $responseInterpreter = new ResponseInterpreter($decoder); - $result = $responseInterpreter->decode("SomePacket"); - $this->assertNull($result); - } - - public function testInvalidResponseCode() { - $message = \Mockery::mock('LibDNS\Messages\Message'); - $message->shouldReceive('getType')->once()->andReturn(\LibDNS\Messages\MessageTypes::RESPONSE); - $message->shouldReceive('getResponseCode')->once()->andReturn(42); - - $decoder = \Mockery::mock('LibDNS\Decoder\Decoder'); - $decoder->shouldReceive('decode')->once()->andReturn($message); - - $responseInterpreter = new ResponseInterpreter($decoder); - $result = $responseInterpreter->decode("SomePacket"); - $this->assertNull($result); - } - - /** - * @group CNAME - */ - public function testNewsBBC() { - $testPacket = $this->getPacketString(self::$bbcNews); - $decoder = (new DecoderFactory)->create(); - - $responseInterpreter = new ResponseInterpreter($decoder); - $decoded = $responseInterpreter->decode($testPacket); - - list($id, $response) = $decoded; - - //Check the IPV4 result - $interpreted = $responseInterpreter->interpret($response, AddressModes::INET4_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals(AddressModes::INET4_ADDR, $type); - //@TODO - this should be multiple - 212.58.246.82 and 212.58.246.83 - $this->assertSame("212.58.246.82", $addr); - $this->assertSame(174, $ttl); - - $interpreted = $responseInterpreter->interpret($response, AddressModes::INET6_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals("newswww.bbc.net.uk", $addr); - $this->assertEquals(AddressModes::CNAME, $type); - $this->assertNull($ttl); - } - - - public function createResponseInterpreter() { - $decoder = (new DecoderFactory)->create(); - $responseInterpreter = new ResponseInterpreter($decoder); - - return $responseInterpreter; - } - - public function testNoResults() { - $responseInterpreter = $this->createResponseInterpreter(); - $packet = $this->getPacketString(self::$standardQueryResponse); - $decoded = $responseInterpreter->decode($packet); - list($id, $message) = $decoded; - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR); - $this->assertNull($interpreted); - - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR); - $this->assertNull($interpreted); - } - - public function testNoSuchName() { - $responseInterpreter = $this->createResponseInterpreter(); - $packet = $this->getPacketString(self::$standardQueryResponseNoSuchName); - $decoded = $responseInterpreter->decode($packet); - list($id, $message) = $decoded; - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR); - $this->assertNull($interpreted, "Response with 'no such name' was not interpreted to null."); - } - - public function testMixed() { - $responseInterpreter = $this->createResponseInterpreter(); - $packet = $this->getPacketString(self::$standardQueryResponseMixed); - $decoded = $responseInterpreter->decode($packet); - list($id, $message) = $decoded; - - //Get the IPv4 part - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals(AddressModes::INET4_ADDR, $type); - $this->assertEquals('204.152.184.88', $addr); - $this->assertEquals(600, $ttl); - - //Get the IPv6 part - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals(AddressModes::INET6_ADDR, $type); - $this->assertEquals('2001:4f8:0:2::d', $addr); - $this->assertEquals(600, $ttl); - } - - public function testIPv4() { - $responseInterpreter = $this->createResponseInterpreter(); - $packet = $this->getPacketString(self::$standardQueryResponseA); - $decoded = $responseInterpreter->decode($packet); - list($id, $message) = $decoded; - - //Get the IPv4 part - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR); - list($type, $addr, $ttl) = $interpreted; - - $this->assertEquals(AddressModes::INET4_ADDR, $type); - $this->assertEquals('204.152.190.12', $addr); - $this->assertEquals(82159, $ttl); - - //Get the IPv6 part - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR); - $this->assertNull($interpreted); - } - - public function testIPv6() { - $responseInterpreter = $this->createResponseInterpreter(); - $packet = $this->getPacketString(self::$standardQueryResponseIPV6); - $decoded = $responseInterpreter->decode($packet); - list($id, $message) = $decoded; - - //Get the IPv4 part - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR); - $this->assertNull($interpreted); - - //Get the IPv6 part - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals(AddressModes::INET6_ADDR, $type); - $this->assertEquals('2001:4f8:4:7:2e0:81ff:fe52:9a6b', $addr); - $this->assertEquals(86340, $ttl); - } - - public function testCnameResponse() { - $responseInterpreter = $this->createResponseInterpreter(); - $packet = $this->getPacketString(self::$standardQueryResponseCNAME); - $decoded = $responseInterpreter->decode($packet); - list($id, $message) = $decoded; - - //Try to get an IPv4 answer - but actually get a CNAME - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals(AddressModes::CNAME, $type); - $this->assertEquals('www.l.google.com', $addr); - - //Try to get an IPv6 answer - but actually get a CNAME - $interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR); - list($type, $addr, $ttl) = $interpreted; - $this->assertEquals(AddressModes::CNAME, $type); - $this->assertEquals('www.l.google.com', $addr); - } -} diff --git a/test/bootstrap.php b/test/bootstrap.php index a763f19..2934f25 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -2,8 +2,8 @@ error_reporting(E_ALL); -if (ini_get('opcache.enable') == true && - ini_get('opcache.save_comments') == false) { +if (ini_get("opcache.enable") == true && + ini_get("opcache.save_comments") == false) { echo "Cannot run tests. OPCache is enabled and is stripping comments, which are required by PHPUnit to provide data for the tests.\n"; exit(-1); } diff --git a/test/fixtures/ipv4Hosts.txt b/test/fixtures/ipv4Hosts.txt deleted file mode 100644 index fa1f64d..0000000 --- a/test/fixtures/ipv4Hosts.txt +++ /dev/null @@ -1,2 +0,0 @@ -192.168.1.1 host1.example.com -192.168.1.2 host2.example.com \ No newline at end of file diff --git a/test/fixtures/ipv6Hosts.txt b/test/fixtures/ipv6Hosts.txt deleted file mode 100644 index 0e6c82c..0000000 --- a/test/fixtures/ipv6Hosts.txt +++ /dev/null @@ -1,3 +0,0 @@ -2001:db8::2:1 host1.example.com -2001:db8:0:1:1:1:1:1 host2.example.com -2001:db8::1:0:0:1 host3.example.com diff --git a/test/fixtures/mixedVersionHosts.txt b/test/fixtures/mixedVersionHosts.txt deleted file mode 100644 index 2de5903..0000000 --- a/test/fixtures/mixedVersionHosts.txt +++ /dev/null @@ -1,14 +0,0 @@ -192.168.1.1 host1.example.com -192.168.1.2 host2.example.com -192.168.1.4 host4.example.com -2001:db8::2:1 host1.example.com -2001:db8:0:1:1:1:1:1 host2.example.com -2001:db8::1:0:0:1 host3.example.com - -#next line is empty for coverage - -//empty_line_for_coverage - - -#next line has invalid address for coverage -1.2.300 doesntexist.com \ No newline at end of file diff --git a/test/fixtures/resolverTest.txt b/test/fixtures/resolverTest.txt deleted file mode 100644 index 19d4270..0000000 --- a/test/fixtures/resolverTest.txt +++ /dev/null @@ -1,8 +0,0 @@ -192.168.1.1 host1.example.com -192.168.1.2 host2.example.com -192.168.1.3 resolvertest -2001:db8::2:1 host1.example.com -2001:db8:0:1:1:1:1:1 host2.example.com -2001:db8::1:0:0:1 host3.example.com - -