Codebase list reactphp-dns / ad76bd8
New upstream version 0.4.13 Dominik George 5 years ago
54 changed file(s) with 4581 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 composer.lock
1 vendor
0 language: php
1
2 php:
3 # - 5.3 # requires old distro, see below
4 - 5.4
5 - 5.5
6 - 5.6
7 - 7.0
8 - 7.1
9 - 7.2
10 - hhvm # ignore errors, see below
11
12 # lock distro so new future defaults will not break the build
13 dist: trusty
14
15 matrix:
16 include:
17 - php: 5.3
18 dist: precise
19 allow_failures:
20 - php: hhvm
21
22 sudo: false
23
24 install:
25 - composer install --no-interaction
26
27 script:
28 - vendor/bin/phpunit --coverage-text
0 # Changelog
1
2 ## 0.4.13 (2018-02-27)
3
4 * Add `Config::loadSystemConfigBlocking()` to load default system config
5 and support parsing DNS config on all supported platforms
6 (`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows)
7 (#92, #93, #94 and #95 by @clue)
8
9 ```php
10 $config = Config::loadSystemConfigBlocking();
11 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
12 ```
13
14 * Remove unneeded cyclic dependency on react/socket
15 (#96 by @clue)
16
17 ## 0.4.12 (2018-01-14)
18
19 * Improve test suite by adding forward compatibility with PHPUnit 6,
20 test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases,
21 add test group to skip integration tests relying on internet connection
22 and add minor documentation improvements.
23 (#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor)
24
25 ## 0.4.11 (2017-08-25)
26
27 * Feature: Support resolving from default hosts file
28 (#75, #76 and #77 by @clue)
29
30 This means that resolving hosts such as `localhost` will now work as
31 expected across all platforms with no changes required:
32
33 ```php
34 $resolver->resolve('localhost')->then(function ($ip) {
35 echo 'IP: ' . $ip;
36 });
37 ```
38
39 The new `HostsExecutor` exists for advanced usage and is otherwise used
40 internally for this feature.
41
42 ## 0.4.10 (2017-08-10)
43
44 * Feature: Forward compatibility with EventLoop v1.0 and v0.5 and
45 lock minimum dependencies and work around circular dependency for tests
46 (#70 and #71 by @clue)
47
48 * Fix: Work around DNS timeout issues for Windows users
49 (#74 by @clue)
50
51 * Documentation and examples for advanced usage
52 (#66 by @WyriHaximus)
53
54 * Remove broken TCP code, do not retry with invalid TCP query
55 (#73 by @clue)
56
57 * Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and
58 lock Travis distro so new defaults will not break the build and
59 fix failing tests for PHP 7.1
60 (#68 by @WyriHaximus and #69 and #72 by @clue)
61
62 ## 0.4.9 (2017-05-01)
63
64 * Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
65 (#61 by @clue)
66
67 ## 0.4.8 (2017-04-16)
68
69 * Feature: Add support for the AAAA record type to the protocol parser
70 (#58 by @othillo)
71
72 * Feature: Add support for the PTR record type to the protocol parser
73 (#59 by @othillo)
74
75 ## 0.4.7 (2017-03-31)
76
77 * Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component
78 (#57 by @clue)
79
80 ## 0.4.6 (2017-03-11)
81
82 * Fix: Fix DNS timeout issues for Windows users and add forward compatibility
83 with Stream v0.5 and upcoming v0.6
84 (#53 by @clue)
85
86 * Improve test suite by adding PHPUnit to `require-dev`
87 (#54 by @clue)
88
89 ## 0.4.5 (2017-03-02)
90
91 * Fix: Ensure we ignore the case of the answer
92 (#51 by @WyriHaximus)
93
94 * Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal
95 code re-use for upcoming versions.
96 (#48 and #49 by @clue)
97
98 ## 0.4.4 (2017-02-13)
99
100 * Fix: Fix handling connection and stream errors
101 (#45 by @clue)
102
103 * Feature: Add examples and forward compatibility with upcoming Socket v0.5 component
104 (#46 and #47 by @clue)
105
106 ## 0.4.3 (2016-07-31)
107
108 * Feature: Allow for cache adapter injection (#38 by @WyriHaximus)
109
110 ```php
111 $factory = new React\Dns\Resolver\Factory();
112
113 $cache = new MyCustomCacheInstance();
114 $resolver = $factory->createCached('8.8.8.8', $loop, $cache);
115 ```
116
117 * Feature: Support Promise cancellation (#35 by @clue)
118
119 ```php
120 $promise = $resolver->resolve('reactphp.org');
121
122 $promise->cancel();
123 ```
124
125 ## 0.4.2 (2016-02-24)
126
127 * Repository maintenance, split off from main repo, improve test suite and documentation
128 * First class support for PHP7 and HHVM (#34 by @clue)
129 * Adjust compatibility to 5.3 (#30 by @clue)
130
131 ## 0.4.1 (2014-04-13)
132
133 * Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus)
134
135 ## 0.4.0 (2014-02-02)
136
137 * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
138 * BC break: Update to React/Promise 2.0
139 * Bug fix: Properly resolve CNAME aliases
140 * Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
141 * Bump React dependencies to v0.4
142
143 ## 0.3.2 (2013-05-10)
144
145 * Feature: Support default port for IPv6 addresses (@clue)
146
147 ## 0.3.0 (2013-04-14)
148
149 * Bump React dependencies to v0.3
150
151 ## 0.2.6 (2012-12-26)
152
153 * Feature: New cache component, used by DNS
154
155 ## 0.2.5 (2012-11-26)
156
157 * Version bump
158
159 ## 0.2.4 (2012-11-18)
160
161 * Feature: Change to promise-based API (@jsor)
162
163 ## 0.2.3 (2012-11-14)
164
165 * Version bump
166
167 ## 0.2.2 (2012-10-28)
168
169 * Feature: DNS executor timeout handling (@arnaud-lb)
170 * Feature: DNS retry executor (@arnaud-lb)
171
172 ## 0.2.1 (2012-10-14)
173
174 * Minor adjustments to DNS parser
175
176 ## 0.2.0 (2012-09-10)
177
178 * Feature: DNS resolver
0 Copyright (c) 2012 Igor Wiedler, Chris Boden
1
2 Permission is hereby granted, free of charge, to any person obtaining a copy
3 of this software and associated documentation files (the "Software"), to deal
4 in the Software without restriction, including without limitation the rights
5 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
6 copies of the Software, and to permit persons to whom the Software is furnished
7 to do so, subject to the following conditions:
8
9 The above copyright notice and this permission notice shall be included in all
10 copies or substantial portions of the Software.
11
12 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
13 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
14 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
15 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
16 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
17 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
18 THE SOFTWARE.
0 # Dns
1
2 [![Build Status](https://travis-ci.org/reactphp/dns.svg?branch=master)](https://travis-ci.org/reactphp/dns)
3
4 Async DNS resolver for [ReactPHP](https://reactphp.org/).
5
6 The main point of the DNS component is to provide async DNS resolution.
7 However, it is really a toolkit for working with DNS messages, and could
8 easily be used to create a DNS server.
9
10 **Table of contents**
11
12 * [Basic usage](#basic-usage)
13 * [Caching](#caching)
14 * [Custom cache adapter](#custom-cache-adapter)
15 * [Advanced usage](#advanced-usage)
16 * [HostsFileExecutor](#hostsfileexecutor)
17 * [Install](#install)
18 * [Tests](#tests)
19 * [License](#license)
20 * [References](#references)
21
22 ## Basic usage
23
24 The most basic usage is to just create a resolver through the resolver
25 factory. All you need to give it is a nameserver, then you can start resolving
26 names, baby!
27
28 ```php
29 $loop = React\EventLoop\Factory::create();
30
31 $config = React\Dns\Config\Config::loadSystemConfigBlocking();
32 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
33
34 $factory = new React\Dns\Resolver\Factory();
35 $dns = $factory->create($server, $loop);
36
37 $dns->resolve('igor.io')->then(function ($ip) {
38 echo "Host: $ip\n";
39 });
40
41 $loop->run();
42 ```
43
44 See also the [first example](examples).
45
46 The `Config` class can be used to load the system default config. This is an
47 operation that may access the filesystem and block. Ideally, this method should
48 thus be executed only once before the loop starts and not repeatedly while it is
49 running.
50 Note that this class may return an *empty* configuration if the system config
51 can not be loaded. As such, you'll likely want to apply a default nameserver
52 as above if none can be found.
53
54 > Note that the factory loads the hosts file from the filesystem once when
55 creating the resolver instance.
56 Ideally, this method should thus be executed only once before the loop starts
57 and not repeatedly while it is running.
58
59 Pending DNS queries can be cancelled by cancelling its pending promise like so:
60
61 ```php
62 $promise = $resolver->resolve('reactphp.org');
63
64 $promise->cancel();
65 ```
66
67 But there's more.
68
69 ## Caching
70
71 You can cache results by configuring the resolver to use a `CachedExecutor`:
72
73 ```php
74 $loop = React\EventLoop\Factory::create();
75
76 $config = React\Dns\Config\Config::loadSystemConfigBlocking();
77 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
78
79 $factory = new React\Dns\Resolver\Factory();
80 $dns = $factory->createCached($server, $loop);
81
82 $dns->resolve('igor.io')->then(function ($ip) {
83 echo "Host: $ip\n";
84 });
85
86 ...
87
88 $dns->resolve('igor.io')->then(function ($ip) {
89 echo "Host: $ip\n";
90 });
91
92 $loop->run();
93 ```
94
95 If the first call returns before the second, only one query will be executed.
96 The second result will be served from an in memory cache.
97 This is particularly useful for long running scripts where the same hostnames
98 have to be looked up multiple times.
99
100 See also the [third example](examples).
101
102 ### Custom cache adapter
103
104 By default, the above will use an in memory cache.
105
106 You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead:
107
108 ```php
109 $cache = new React\Cache\ArrayCache();
110 $loop = React\EventLoop\Factory::create();
111 $factory = new React\Dns\Resolver\Factory();
112 $dns = $factory->createCached('8.8.8.8', $loop, $cache);
113 ```
114
115 See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
116
117 ## Advanced Usage
118
119 For more advanced usages one can utilize the `React\Dns\Query\Executor` directly.
120 The following example looks up the `IPv6` address for `igor.io`.
121
122 ```php
123 $loop = Factory::create();
124
125 $executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
126
127 $executor->query(
128 '8.8.8.8:53',
129 new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time())
130 )->done(function (Message $message) {
131 foreach ($message->answers as $answer) {
132 echo 'IPv6: ' . $answer->data . PHP_EOL;
133 }
134 }, 'printf');
135
136 $loop->run();
137
138 ```
139
140 See also the [fourth example](examples).
141
142 ### HostsFileExecutor
143
144 Note that the above `Executor` class always performs an actual DNS query.
145 If you also want to take entries from your hosts file into account, you may
146 use this code:
147
148 ```php
149 $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
150
151 $executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
152 $executor = new HostsFileExecutor($hosts, $executor);
153
154 $executor->query(
155 '8.8.8.8:53',
156 new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time())
157 );
158 ```
159
160 ## Install
161
162 The recommended way to install this library is [through Composer](https://getcomposer.org).
163 [New to Composer?](https://getcomposer.org/doc/00-intro.md)
164
165 This will install the latest supported version:
166
167 ```bash
168 $ composer require react/dns:^0.4.13
169 ```
170
171 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
172
173 This project aims to run on any platform and thus does not require any PHP
174 extensions and supports running on legacy PHP 5.3 through current PHP 7+ and
175 HHVM.
176 It's *highly recommended to use PHP 7+* for this project.
177
178 ## Tests
179
180 To run the test suite, you first need to clone this repo and then install all
181 dependencies [through Composer](https://getcomposer.org):
182
183 ```bash
184 $ composer install
185 ```
186
187 To run the test suite, go to the project root and run:
188
189 ```bash
190 $ php vendor/bin/phpunit
191 ```
192
193 The test suite also contains a number of functional integration tests that rely
194 on a stable internet connection.
195 If you do not want to run these, they can simply be skipped like this:
196
197 ```bash
198 $ php vendor/bin/phpunit --exclude-group internet
199 ```
200
201 ## License
202
203 MIT, see [LICENSE file](LICENSE).
204
205 ## References
206
207 * [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
208 * [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification
0 {
1 "name": "react/dns",
2 "description": "Async DNS resolver for ReactPHP",
3 "keywords": ["dns", "dns-resolver", "ReactPHP", "async"],
4 "license": "MIT",
5 "require": {
6 "php": ">=5.3.0",
7 "react/cache": "~0.4.0|~0.3.0",
8 "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5",
9 "react/promise": "^2.1 || ^1.2.1",
10 "react/promise-timer": "^1.2",
11 "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5"
12 },
13 "require-dev": {
14 "clue/block-react": "^1.2",
15 "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35"
16 },
17 "autoload": {
18 "psr-4": { "React\\Dns\\": "src" }
19 },
20 "autoload-dev": {
21 "psr-4": { "React\\Tests\\Dns\\": "tests" }
22 }
23 }
0 <?php
1
2 use React\Dns\Config\Config;
3 use React\Dns\Resolver\Factory;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 $loop = React\EventLoop\Factory::create();
8
9 $config = Config::loadSystemConfigBlocking();
10 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
11
12 $factory = new Factory();
13 $resolver = $factory->create($server, $loop);
14
15 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
16
17 $resolver->resolve($name)->then(function ($ip) use ($name) {
18 echo 'IP for ' . $name . ': ' . $ip . PHP_EOL;
19 }, 'printf');
20
21 $loop->run();
0 <?php
1
2 use React\Dns\Config\Config;
3 use React\Dns\Resolver\Factory;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 $loop = React\EventLoop\Factory::create();
8
9 $config = Config::loadSystemConfigBlocking();
10 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
11
12 $factory = new Factory();
13 $resolver = $factory->create($server, $loop);
14
15 $names = array_slice($argv, 1);
16 if (!$names) {
17 $names = array('google.com', 'www.google.com', 'gmail.com');
18 }
19
20 foreach ($names as $name) {
21 $resolver->resolve($name)->then(function ($ip) use ($name) {
22 echo 'IP for ' . $name . ': ' . $ip . PHP_EOL;
23 }, 'printf');
24 }
25
26 $loop->run();
0 <?php
1
2 use React\Dns\Config\Config;
3 use React\Dns\Resolver\Factory;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 $loop = React\EventLoop\Factory::create();
8
9 $config = Config::loadSystemConfigBlocking();
10 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
11
12 $factory = new Factory();
13 $resolver = $factory->createCached($server, $loop);
14
15 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
16
17 $resolver->resolve($name)->then(function ($ip) use ($name) {
18 echo 'IP for ' . $name . ': ' . $ip . PHP_EOL;
19 }, 'printf');
20
21 $loop->addTimer(1.0, function() use ($name, $resolver) {
22 $resolver->resolve($name)->then(function ($ip) use ($name) {
23 echo 'IP for ' . $name . ': ' . $ip . PHP_EOL;
24 }, 'printf');
25 });
26
27 $loop->addTimer(2.0, function() use ($name, $resolver) {
28 $resolver->resolve($name)->then(function ($ip) use ($name) {
29 echo 'IP for ' . $name . ': ' . $ip . PHP_EOL;
30 }, 'printf');
31 });
32
33 $loop->addTimer(3.0, function() use ($name, $resolver) {
34 $resolver->resolve($name)->then(function ($ip) use ($name) {
35 echo 'IP for ' . $name . ': ' . $ip . PHP_EOL;
36 }, 'printf');
37 });
38
39 $loop->run();
0 <?php
1
2 use React\Dns\Model\Message;
3 use React\Dns\Protocol\BinaryDumper;
4 use React\Dns\Protocol\Parser;
5 use React\Dns\Query\Executor;
6 use React\Dns\Query\Query;
7 use React\EventLoop\Factory;
8
9 require __DIR__ . '/../vendor/autoload.php';
10
11 $loop = Factory::create();
12
13 $executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
14
15 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
16
17 $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN, time());
18 $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time());
19
20 $executor->query('8.8.8.8:53', $ipv4Query)->done(function (Message $message) {
21 foreach ($message->answers as $answer) {
22 echo 'IPv4: ' . $answer->data . PHP_EOL;
23 }
24 }, 'printf');
25 $executor->query('8.8.8.8:53', $ipv6Query)->done(function (Message $message) {
26 foreach ($message->answers as $answer) {
27 echo 'IPv6: ' . $answer->data . PHP_EOL;
28 }
29 }, 'printf');
30
31 $loop->run();
0 <?xml version="1.0" encoding="UTF-8"?>
1
2 <phpunit backupGlobals="false"
3 backupStaticAttributes="false"
4 colors="true"
5 convertErrorsToExceptions="true"
6 convertNoticesToExceptions="true"
7 convertWarningsToExceptions="true"
8 processIsolation="false"
9 stopOnFailure="false"
10 syntaxCheck="false"
11 bootstrap="vendor/autoload.php"
12 >
13 <testsuites>
14 <testsuite name="React Test Suite">
15 <directory>./tests/</directory>
16 </testsuite>
17 </testsuites>
18
19 <filter>
20 <whitelist>
21 <directory>./src/</directory>
22 </whitelist>
23 </filter>
24 </phpunit>
0 <?php
1
2 namespace React\Dns;
3
4 class BadServerException extends \Exception
5 {
6 }
0 <?php
1
2 namespace React\Dns\Config;
3
4 use RuntimeException;
5
6 class Config
7 {
8 /**
9 * Loads the system DNS configuration
10 *
11 * Note that this method may block while loading its internal files and/or
12 * commands and should thus be used with care! While this should be
13 * relatively fast for most systems, it remains unknown if this may block
14 * under certain circumstances. In particular, this method should only be
15 * executed before the loop starts, not while it is running.
16 *
17 * Note that this method will try to access its files and/or commands and
18 * try to parse its output. Currently, this will only parse valid nameserver
19 * entries from its output and will ignore all other output without
20 * complaining.
21 *
22 * Note that the previous section implies that this may return an empty
23 * `Config` object if no valid nameserver entries can be found.
24 *
25 * @return self
26 * @codeCoverageIgnore
27 */
28 public static function loadSystemConfigBlocking()
29 {
30 // Use WMIC output on Windows
31 if (DIRECTORY_SEPARATOR === '\\') {
32 return self::loadWmicBlocking();
33 }
34
35 // otherwise (try to) load from resolv.conf
36 try {
37 return self::loadResolvConfBlocking();
38 } catch (RuntimeException $ignored) {
39 // return empty config if parsing fails (file not found)
40 return new self();
41 }
42 }
43
44 /**
45 * Loads a resolv.conf file (from the given path or default location)
46 *
47 * Note that this method blocks while loading the given path and should
48 * thus be used with care! While this should be relatively fast for normal
49 * resolv.conf files, this may be an issue if this file is located on a slow
50 * device or contains an excessive number of entries. In particular, this
51 * method should only be executed before the loop starts, not while it is
52 * running.
53 *
54 * Note that this method will throw if the given file can not be loaded,
55 * such as if it is not readable or does not exist. In particular, this file
56 * is not available on Windows.
57 *
58 * Currently, this will only parse valid "nameserver X" lines from the
59 * given file contents. Lines can be commented out with "#" and ";" and
60 * invalid lines will be ignored without complaining. See also
61 * `man resolv.conf` for more details.
62 *
63 * Note that the previous section implies that this may return an empty
64 * `Config` object if no valid "nameserver X" lines can be found. See also
65 * `man resolv.conf` which suggests that the DNS server on the localhost
66 * should be used in this case. This is left up to higher level consumers
67 * of this API.
68 *
69 * @param ?string $path (optional) path to resolv.conf file or null=load default location
70 * @return self
71 * @throws RuntimeException if the path can not be loaded (does not exist)
72 */
73 public static function loadResolvConfBlocking($path = null)
74 {
75 if ($path === null) {
76 $path = '/etc/resolv.conf';
77 }
78
79 $contents = @file_get_contents($path);
80 if ($contents === false) {
81 throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"');
82 }
83
84 preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches);
85
86 $config = new self();
87 $config->nameservers = $matches[1];
88
89 return $config;
90 }
91
92 /**
93 * Loads the DNS configurations from Windows's WMIC (from the given command or default command)
94 *
95 * Note that this method blocks while loading the given command and should
96 * thus be used with care! While this should be relatively fast for normal
97 * WMIC commands, it remains unknown if this may block under certain
98 * circumstances. In particular, this method should only be executed before
99 * the loop starts, not while it is running.
100 *
101 * Note that this method will only try to execute the given command try to
102 * parse its output, irrespective of whether this command exists. In
103 * particular, this command is only available on Windows. Currently, this
104 * will only parse valid nameserver entries from the command output and will
105 * ignore all other output without complaining.
106 *
107 * Note that the previous section implies that this may return an empty
108 * `Config` object if no valid nameserver entries can be found.
109 *
110 * @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
111 * @return self
112 * @link https://ss64.com/nt/wmic.html
113 */
114 public static function loadWmicBlocking($command = null)
115 {
116 $contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
117 preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
118
119 $config = new self();
120 $config->nameservers = $matches[1];
121
122 return $config;
123 }
124
125 public $nameservers = array();
126 }
0 <?php
1
2 namespace React\Dns\Config;
3
4 use React\EventLoop\LoopInterface;
5 use React\Promise;
6 use React\Promise\Deferred;
7 use React\Stream\ReadableResourceStream;
8 use React\Stream\Stream;
9
10 /**
11 * @deprecated
12 * @see Config see Config class instead.
13 */
14 class FilesystemFactory
15 {
16 private $loop;
17
18 public function __construct(LoopInterface $loop)
19 {
20 $this->loop = $loop;
21 }
22
23 public function create($filename)
24 {
25 return $this
26 ->loadEtcResolvConf($filename)
27 ->then(array($this, 'parseEtcResolvConf'));
28 }
29
30 /**
31 * @param string $contents
32 * @return Promise
33 * @deprecated see Config instead
34 */
35 public function parseEtcResolvConf($contents)
36 {
37 return Promise\resolve(Config::loadResolvConfBlocking(
38 'data://text/plain;base64,' . base64_encode($contents)
39 ));
40 }
41
42 public function loadEtcResolvConf($filename)
43 {
44 if (!file_exists($filename)) {
45 return Promise\reject(new \InvalidArgumentException("The filename for /etc/resolv.conf given does not exist: $filename"));
46 }
47
48 try {
49 $deferred = new Deferred();
50
51 $fd = fopen($filename, 'r');
52 stream_set_blocking($fd, 0);
53
54 $contents = '';
55
56 $stream = class_exists('React\Stream\ReadableResourceStream') ? new ReadableResourceStream($fd, $this->loop) : new Stream($fd, $this->loop);
57 $stream->on('data', function ($data) use (&$contents) {
58 $contents .= $data;
59 });
60 $stream->on('end', function () use (&$contents, $deferred) {
61 $deferred->resolve($contents);
62 });
63 $stream->on('error', function ($error) use ($deferred) {
64 $deferred->reject($error);
65 });
66
67 return $deferred->promise();
68 } catch (\Exception $e) {
69 return Promise\reject($e);
70 }
71 }
72 }
0 <?php
1
2 namespace React\Dns\Config;
3
4 use RuntimeException;
5
6 /**
7 * Represents a static hosts file which maps hostnames to IPs
8 *
9 * Hosts files are used on most systems to avoid actually hitting the DNS for
10 * certain common hostnames.
11 *
12 * Most notably, this file usually contains an entry to map "localhost" to the
13 * local IP. Windows is a notable exception here, as Windows does not actually
14 * include "localhost" in this file by default. To compensate for this, this
15 * class may explicitly be wrapped in another HostsFile instance which
16 * hard-codes these entries for Windows (see also Factory).
17 *
18 * This class mostly exists to abstract the parsing/extraction process so this
19 * can be replaced with a faster alternative in the future.
20 */
21 class HostsFile
22 {
23 /**
24 * Returns the default path for the hosts file on this system
25 *
26 * @return string
27 * @codeCoverageIgnore
28 */
29 public static function getDefaultPath()
30 {
31 // use static path for all Unix-based systems
32 if (DIRECTORY_SEPARATOR !== '\\') {
33 return '/etc/hosts';
34 }
35
36 // Windows actually stores the path in the registry under
37 // \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath
38 $path = '%SystemRoot%\\system32\drivers\etc\hosts';
39
40 $base = getenv('SystemRoot');
41 if ($base === false) {
42 $base = 'C:\\Windows';
43 }
44
45 return str_replace('%SystemRoot%', $base, $path);
46 }
47
48 /**
49 * Loads a hosts file (from the given path or default location)
50 *
51 * Note that this method blocks while loading the given path and should
52 * thus be used with care! While this should be relatively fast for normal
53 * hosts file, this may be an issue if this file is located on a slow device
54 * or contains an excessive number of entries. In particular, this method
55 * should only be executed before the loop starts, not while it is running.
56 *
57 * @param ?string $path (optional) path to hosts file or null=load default location
58 * @return self
59 * @throws RuntimeException if the path can not be loaded (does not exist)
60 */
61 public static function loadFromPathBlocking($path = null)
62 {
63 if ($path === null) {
64 $path = self::getDefaultPath();
65 }
66
67 $contents = @file_get_contents($path);
68 if ($contents === false) {
69 throw new RuntimeException('Unable to load hosts file "' . $path . '"');
70 }
71
72 return new self($contents);
73 }
74
75 /**
76 * Instantiate new hosts file with the given hosts file contents
77 *
78 * @param string $contents
79 */
80 public function __construct($contents)
81 {
82 // remove all comments from the contents
83 $contents = preg_replace('/[ \t]*#.*/', '', strtolower($contents));
84
85 $this->contents = $contents;
86 }
87
88 /**
89 * Returns all IPs for the given hostname
90 *
91 * @param string $name
92 * @return string[]
93 */
94 public function getIpsForHost($name)
95 {
96 $name = strtolower($name);
97
98 $ips = array();
99 foreach (preg_split('/\r?\n/', $this->contents) as $line) {
100 $parts = preg_split('/\s+/', $line);
101 $ip = array_shift($parts);
102 if ($parts && array_search($name, $parts) !== false) {
103 // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
104 if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
105 $ip = substr($ip, 0, $pos);
106 }
107
108 if (@inet_pton($ip) !== false) {
109 $ips[] = $ip;
110 }
111 }
112 }
113
114 return $ips;
115 }
116
117 /**
118 * Returns all hostnames for the given IPv4 or IPv6 address
119 *
120 * @param string $ip
121 * @return string[]
122 */
123 public function getHostsForIp($ip)
124 {
125 // check binary representation of IP to avoid string case and short notation
126 $ip = @inet_pton($ip);
127 if ($ip === false) {
128 return array();
129 }
130
131 $names = array();
132 foreach (preg_split('/\r?\n/', $this->contents) as $line) {
133 $parts = preg_split('/\s+/', $line, null, PREG_SPLIT_NO_EMPTY);
134 $addr = array_shift($parts);
135
136 // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
137 if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) {
138 $addr = substr($addr, 0, $pos);
139 }
140
141 if (@inet_pton($addr) === $ip) {
142 foreach ($parts as $part) {
143 $names[] = $part;
144 }
145 }
146 }
147
148 return $names;
149 }
150 }
0 <?php
1
2 namespace React\Dns\Model;
3
4 class HeaderBag
5 {
6 public $data = '';
7
8 public $attributes = array(
9 'qdCount' => 0,
10 'anCount' => 0,
11 'nsCount' => 0,
12 'arCount' => 0,
13 'qr' => 0,
14 'opcode' => Message::OPCODE_QUERY,
15 'aa' => 0,
16 'tc' => 0,
17 'rd' => 0,
18 'ra' => 0,
19 'z' => 0,
20 'rcode' => Message::RCODE_OK,
21 );
22
23 public function get($name)
24 {
25 return isset($this->attributes[$name]) ? $this->attributes[$name] : null;
26 }
27
28 public function set($name, $value)
29 {
30 $this->attributes[$name] = $value;
31 }
32
33 public function isQuery()
34 {
35 return 0 === $this->attributes['qr'];
36 }
37
38 public function isResponse()
39 {
40 return 1 === $this->attributes['qr'];
41 }
42
43 public function isTruncated()
44 {
45 return 1 === $this->attributes['tc'];
46 }
47
48 public function populateCounts(Message $message)
49 {
50 $this->attributes['qdCount'] = count($message->questions);
51 $this->attributes['anCount'] = count($message->answers);
52 $this->attributes['nsCount'] = count($message->authority);
53 $this->attributes['arCount'] = count($message->additional);
54 }
55 }
0 <?php
1
2 namespace React\Dns\Model;
3
4 use React\Dns\Query\Query;
5 use React\Dns\Model\Record;
6
7 class Message
8 {
9 const TYPE_A = 1;
10 const TYPE_NS = 2;
11 const TYPE_CNAME = 5;
12 const TYPE_SOA = 6;
13 const TYPE_PTR = 12;
14 const TYPE_MX = 15;
15 const TYPE_TXT = 16;
16 const TYPE_AAAA = 28;
17
18 const CLASS_IN = 1;
19
20 const OPCODE_QUERY = 0;
21 const OPCODE_IQUERY = 1; // inverse query
22 const OPCODE_STATUS = 2;
23
24 const RCODE_OK = 0;
25 const RCODE_FORMAT_ERROR = 1;
26 const RCODE_SERVER_FAILURE = 2;
27 const RCODE_NAME_ERROR = 3;
28 const RCODE_NOT_IMPLEMENTED = 4;
29 const RCODE_REFUSED = 5;
30
31 /**
32 * Creates a new request message for the given query
33 *
34 * @param Query $query
35 * @return self
36 */
37 public static function createRequestForQuery(Query $query)
38 {
39 $request = new Message();
40 $request->header->set('id', self::generateId());
41 $request->header->set('rd', 1);
42 $request->questions[] = (array) $query;
43 $request->prepare();
44
45 return $request;
46 }
47
48 /**
49 * Creates a new response message for the given query with the given answer records
50 *
51 * @param Query $query
52 * @param Record[] $answers
53 * @return self
54 */
55 public static function createResponseWithAnswersForQuery(Query $query, array $answers)
56 {
57 $response = new Message();
58 $response->header->set('id', self::generateId());
59 $response->header->set('qr', 1);
60 $response->header->set('opcode', Message::OPCODE_QUERY);
61 $response->header->set('rd', 1);
62 $response->header->set('rcode', Message::RCODE_OK);
63
64 $response->questions[] = (array) $query;
65
66 foreach ($answers as $record) {
67 $response->answers[] = $record;
68 }
69
70 $response->prepare();
71
72 return $response;
73 }
74
75 private static function generateId()
76 {
77 return mt_rand(0, 0xffff);
78 }
79
80 public $data = '';
81
82 public $header;
83 public $questions = array();
84 public $answers = array();
85 public $authority = array();
86 public $additional = array();
87
88 public $consumed = 0;
89
90 public function __construct()
91 {
92 $this->header = new HeaderBag();
93 }
94
95 public function prepare()
96 {
97 $this->header->populateCounts($this);
98 }
99 }
0 <?php
1
2 namespace React\Dns\Model;
3
4 class Record
5 {
6 public $name;
7 public $type;
8 public $class;
9 public $ttl;
10 public $data;
11
12 public function __construct($name, $type, $class, $ttl = 0, $data = null)
13 {
14 $this->name = $name;
15 $this->type = $type;
16 $this->class = $class;
17 $this->ttl = $ttl;
18 $this->data = $data;
19 }
20 }
0 <?php
1
2 namespace React\Dns\Protocol;
3
4 use React\Dns\Model\Message;
5 use React\Dns\Model\HeaderBag;
6
7 class BinaryDumper
8 {
9 public function toBinary(Message $message)
10 {
11 $data = '';
12
13 $data .= $this->headerToBinary($message->header);
14 $data .= $this->questionToBinary($message->questions);
15
16 return $data;
17 }
18
19 private function headerToBinary(HeaderBag $header)
20 {
21 $data = '';
22
23 $data .= pack('n', $header->get('id'));
24
25 $flags = 0x00;
26 $flags = ($flags << 1) | $header->get('qr');
27 $flags = ($flags << 4) | $header->get('opcode');
28 $flags = ($flags << 1) | $header->get('aa');
29 $flags = ($flags << 1) | $header->get('tc');
30 $flags = ($flags << 1) | $header->get('rd');
31 $flags = ($flags << 1) | $header->get('ra');
32 $flags = ($flags << 3) | $header->get('z');
33 $flags = ($flags << 4) | $header->get('rcode');
34
35 $data .= pack('n', $flags);
36
37 $data .= pack('n', $header->get('qdCount'));
38 $data .= pack('n', $header->get('anCount'));
39 $data .= pack('n', $header->get('nsCount'));
40 $data .= pack('n', $header->get('arCount'));
41
42 return $data;
43 }
44
45 private function questionToBinary(array $questions)
46 {
47 $data = '';
48
49 foreach ($questions as $question) {
50 $labels = explode('.', $question['name']);
51 foreach ($labels as $label) {
52 $data .= chr(strlen($label)).$label;
53 }
54 $data .= "\x00";
55
56 $data .= pack('n*', $question['type'], $question['class']);
57 }
58
59 return $data;
60 }
61 }
0 <?php
1
2 namespace React\Dns\Protocol;
3
4 use React\Dns\Model\Message;
5 use React\Dns\Model\Record;
6 use InvalidArgumentException;
7
8 /**
9 * DNS protocol parser
10 *
11 * Obsolete and uncommon types and classes are not implemented.
12 */
13 class Parser
14 {
15 /**
16 * Parses the given raw binary message into a Message object
17 *
18 * @param string $data
19 * @throws InvalidArgumentException
20 * @return Message
21 */
22 public function parseMessage($data)
23 {
24 $message = new Message();
25 if ($this->parse($data, $message) !== $message) {
26 throw new InvalidArgumentException('Unable to parse binary message');
27 }
28
29 return $message;
30 }
31
32 /**
33 * @deprecated unused, exists for BC only
34 */
35 public function parseChunk($data, Message $message)
36 {
37 return $this->parse($data, $message);
38 }
39
40 private function parse($data, Message $message)
41 {
42 $message->data .= $data;
43
44 if (!$message->header->get('id')) {
45 if (!$this->parseHeader($message)) {
46 return;
47 }
48 }
49
50 if ($message->header->get('qdCount') != count($message->questions)) {
51 if (!$this->parseQuestion($message)) {
52 return;
53 }
54 }
55
56 if ($message->header->get('anCount') != count($message->answers)) {
57 if (!$this->parseAnswer($message)) {
58 return;
59 }
60 }
61
62 return $message;
63 }
64
65 public function parseHeader(Message $message)
66 {
67 if (strlen($message->data) < 12) {
68 return;
69 }
70
71 $header = substr($message->data, 0, 12);
72 $message->consumed += 12;
73
74 list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header));
75
76 $rcode = $fields & bindec('1111');
77 $z = ($fields >> 4) & bindec('111');
78 $ra = ($fields >> 7) & 1;
79 $rd = ($fields >> 8) & 1;
80 $tc = ($fields >> 9) & 1;
81 $aa = ($fields >> 10) & 1;
82 $opcode = ($fields >> 11) & bindec('1111');
83 $qr = ($fields >> 15) & 1;
84
85 $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount',
86 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode');
87
88
89 foreach ($vars as $name => $value) {
90 $message->header->set($name, $value);
91 }
92
93 return $message;
94 }
95
96 public function parseQuestion(Message $message)
97 {
98 if (strlen($message->data) < 2) {
99 return;
100 }
101
102 $consumed = $message->consumed;
103
104 list($labels, $consumed) = $this->readLabels($message->data, $consumed);
105
106 if (null === $labels) {
107 return;
108 }
109
110 if (strlen($message->data) - $consumed < 4) {
111 return;
112 }
113
114 list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
115 $consumed += 4;
116
117 $message->consumed = $consumed;
118
119 $message->questions[] = array(
120 'name' => implode('.', $labels),
121 'type' => $type,
122 'class' => $class,
123 );
124
125 if ($message->header->get('qdCount') != count($message->questions)) {
126 return $this->parseQuestion($message);
127 }
128
129 return $message;
130 }
131
132 public function parseAnswer(Message $message)
133 {
134 if (strlen($message->data) < 2) {
135 return;
136 }
137
138 $consumed = $message->consumed;
139
140 list($labels, $consumed) = $this->readLabels($message->data, $consumed);
141
142 if (null === $labels) {
143 return;
144 }
145
146 if (strlen($message->data) - $consumed < 10) {
147 return;
148 }
149
150 list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
151 $consumed += 4;
152
153 list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4)));
154 $consumed += 4;
155
156 list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2)));
157 $consumed += 2;
158
159 $rdata = null;
160
161 if (Message::TYPE_A === $type || Message::TYPE_AAAA === $type) {
162 $ip = substr($message->data, $consumed, $rdLength);
163 $consumed += $rdLength;
164
165 $rdata = inet_ntop($ip);
166 }
167
168 if (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type) {
169 list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed);
170
171 $rdata = implode('.', $bodyLabels);
172 }
173
174 $message->consumed = $consumed;
175
176 $name = implode('.', $labels);
177 $ttl = $this->signedLongToUnsignedLong($ttl);
178 $record = new Record($name, $type, $class, $ttl, $rdata);
179
180 $message->answers[] = $record;
181
182 if ($message->header->get('anCount') != count($message->answers)) {
183 return $this->parseAnswer($message);
184 }
185
186 return $message;
187 }
188
189 private function readLabels($data, $consumed)
190 {
191 $labels = array();
192
193 while (true) {
194 if ($this->isEndOfLabels($data, $consumed)) {
195 $consumed += 1;
196 break;
197 }
198
199 if ($this->isCompressedLabel($data, $consumed)) {
200 list($newLabels, $consumed) = $this->getCompressedLabel($data, $consumed);
201 $labels = array_merge($labels, $newLabels);
202 break;
203 }
204
205 $length = ord(substr($data, $consumed, 1));
206 $consumed += 1;
207
208 if (strlen($data) - $consumed < $length) {
209 return array(null, null);
210 }
211
212 $labels[] = substr($data, $consumed, $length);
213 $consumed += $length;
214 }
215
216 return array($labels, $consumed);
217 }
218
219 public function isEndOfLabels($data, $consumed)
220 {
221 $length = ord(substr($data, $consumed, 1));
222 return 0 === $length;
223 }
224
225 public function getCompressedLabel($data, $consumed)
226 {
227 list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed);
228 list($labels) = $this->readLabels($data, $nameOffset);
229
230 return array($labels, $consumed);
231 }
232
233 public function isCompressedLabel($data, $consumed)
234 {
235 $mask = 0xc000; // 1100000000000000
236 list($peek) = array_values(unpack('n', substr($data, $consumed, 2)));
237
238 return (bool) ($peek & $mask);
239 }
240
241 public function getCompressedLabelOffset($data, $consumed)
242 {
243 $mask = 0x3fff; // 0011111111111111
244 list($peek) = array_values(unpack('n', substr($data, $consumed, 2)));
245
246 return array($peek & $mask, $consumed + 2);
247 }
248
249 public function signedLongToUnsignedLong($i)
250 {
251 return $i & 0x80000000 ? $i - 0xffffffff : $i;
252 }
253 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\Dns\Model\Message;
5
6 class CachedExecutor implements ExecutorInterface
7 {
8 private $executor;
9 private $cache;
10
11 public function __construct(ExecutorInterface $executor, RecordCache $cache)
12 {
13 $this->executor = $executor;
14 $this->cache = $cache;
15 }
16
17 public function query($nameserver, Query $query)
18 {
19 $executor = $this->executor;
20 $cache = $this->cache;
21
22 return $this->cache
23 ->lookup($query)
24 ->then(
25 function ($cachedRecords) use ($query) {
26 return Message::createResponseWithAnswersForQuery($query, $cachedRecords);
27 },
28 function () use ($executor, $cache, $nameserver, $query) {
29 return $executor
30 ->query($nameserver, $query)
31 ->then(function ($response) use ($cache, $query) {
32 $cache->storeResponseMessage($query->currentTime, $response);
33 return $response;
34 });
35 }
36 );
37 }
38
39 /**
40 * @deprecated unused, exists for BC only
41 */
42 public function buildResponse(Query $query, array $cachedRecords)
43 {
44 return Message::createResponseWithAnswersForQuery($query, $cachedRecords);
45 }
46
47 /**
48 * @deprecated unused, exists for BC only
49 */
50 protected function generateId()
51 {
52 return mt_rand(0, 0xffff);
53 }
54 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 class CancellationException extends \RuntimeException
5 {
6 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\Dns\Model\Message;
5 use React\Dns\Protocol\Parser;
6 use React\Dns\Protocol\BinaryDumper;
7 use React\EventLoop\LoopInterface;
8 use React\Promise\Deferred;
9 use React\Promise;
10 use React\Stream\DuplexResourceStream;
11 use React\Stream\Stream;
12
13 class Executor implements ExecutorInterface
14 {
15 private $loop;
16 private $parser;
17 private $dumper;
18 private $timeout;
19
20 /**
21 *
22 * Note that albeit supported, the $timeout parameter is deprecated!
23 * You should pass a `null` value here instead. If you need timeout handling,
24 * use the `TimeoutConnector` instead.
25 *
26 * @param LoopInterface $loop
27 * @param Parser $parser
28 * @param BinaryDumper $dumper
29 * @param null|float $timeout DEPRECATED: timeout for DNS query or NULL=no timeout
30 */
31 public function __construct(LoopInterface $loop, Parser $parser, BinaryDumper $dumper, $timeout = 5)
32 {
33 $this->loop = $loop;
34 $this->parser = $parser;
35 $this->dumper = $dumper;
36 $this->timeout = $timeout;
37 }
38
39 public function query($nameserver, Query $query)
40 {
41 $request = Message::createRequestForQuery($query);
42
43 $queryData = $this->dumper->toBinary($request);
44 $transport = strlen($queryData) > 512 ? 'tcp' : 'udp';
45
46 return $this->doQuery($nameserver, $transport, $queryData, $query->name);
47 }
48
49 /**
50 * @deprecated unused, exists for BC only
51 */
52 public function prepareRequest(Query $query)
53 {
54 return Message::createRequestForQuery($query);
55 }
56
57 public function doQuery($nameserver, $transport, $queryData, $name)
58 {
59 // we only support UDP right now
60 if ($transport !== 'udp') {
61 return Promise\reject(new \RuntimeException(
62 'DNS query for ' . $name . ' failed: Requested transport "' . $transport . '" not available, only UDP is supported in this version'
63 ));
64 }
65
66 $that = $this;
67 $parser = $this->parser;
68 $loop = $this->loop;
69
70 // UDP connections are instant, so try this without a timer
71 try {
72 $conn = $this->createConnection($nameserver, $transport);
73 } catch (\Exception $e) {
74 return Promise\reject(new \RuntimeException('DNS query for ' . $name . ' failed: ' . $e->getMessage(), 0, $e));
75 }
76
77 $deferred = new Deferred(function ($resolve, $reject) use (&$timer, $loop, &$conn, $name) {
78 $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name)));
79
80 if ($timer !== null) {
81 $loop->cancelTimer($timer);
82 }
83 $conn->close();
84 });
85
86 $timer = null;
87 if ($this->timeout !== null) {
88 $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) {
89 $conn->close();
90 $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name)));
91 });
92 }
93
94 $conn->on('data', function ($data) use ($conn, $parser, $deferred, $timer, $loop, $name) {
95 $conn->end();
96 if ($timer !== null) {
97 $loop->cancelTimer($timer);
98 }
99
100 try {
101 $response = $parser->parseMessage($data);
102 } catch (\Exception $e) {
103 $deferred->reject($e);
104 return;
105 }
106
107 if ($response->header->isTruncated()) {
108 $deferred->reject(new \RuntimeException('DNS query for ' . $name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported'));
109 return;
110 }
111
112 $deferred->resolve($response);
113 });
114 $conn->write($queryData);
115
116 return $deferred->promise();
117 }
118
119 /**
120 * @deprecated unused, exists for BC only
121 */
122 protected function generateId()
123 {
124 return mt_rand(0, 0xffff);
125 }
126
127 /**
128 * @param string $nameserver
129 * @param string $transport
130 * @return \React\Stream\DuplexStreamInterface
131 */
132 protected function createConnection($nameserver, $transport)
133 {
134 $fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT);
135 if ($fd === false) {
136 throw new \RuntimeException('Unable to connect to DNS server: ' . $errstr, $errno);
137 }
138
139 // Instantiate stream instance around this stream resource.
140 // This ought to be replaced with a datagram socket in the future.
141 // Temporary work around for Windows 10: buffer whole UDP response
142 // @coverageIgnoreStart
143 if (!class_exists('React\Stream\Stream')) {
144 // prefer DuplexResourceStream as of react/stream v0.7.0
145 $conn = new DuplexResourceStream($fd, $this->loop, -1);
146 } else {
147 // use legacy Stream class for react/stream < v0.7.0
148 $conn = new Stream($fd, $this->loop);
149 $conn->bufferSize = null;
150 }
151 // @coverageIgnoreEnd
152
153 return $conn;
154 }
155 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 interface ExecutorInterface
5 {
6 public function query($nameserver, Query $query);
7 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\Dns\Config\HostsFile;
5 use React\Dns\Model\Message;
6 use React\Dns\Model\Record;
7 use React\Promise;
8
9 /**
10 * Resolves hosts from the givne HostsFile or falls back to another executor
11 *
12 * If the host is found in the hosts file, it will not be passed to the actual
13 * DNS executor. If the host is not found in the hosts file, it will be passed
14 * to the DNS executor as a fallback.
15 */
16 class HostsFileExecutor implements ExecutorInterface
17 {
18 private $hosts;
19 private $fallback;
20
21 public function __construct(HostsFile $hosts, ExecutorInterface $fallback)
22 {
23 $this->hosts = $hosts;
24 $this->fallback = $fallback;
25 }
26
27 public function query($nameserver, Query $query)
28 {
29 if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
30 // forward lookup for type A or AAAA
31 $records = array();
32 $expectsColon = $query->type === Message::TYPE_AAAA;
33 foreach ($this->hosts->getIpsForHost($query->name) as $ip) {
34 // ensure this is an IPv4/IPV6 address according to query type
35 if ((strpos($ip, ':') !== false) === $expectsColon) {
36 $records[] = new Record($query->name, $query->type, $query->class, 0, $ip);
37 }
38 }
39
40 if ($records) {
41 return Promise\resolve(
42 Message::createResponseWithAnswersForQuery($query, $records)
43 );
44 }
45 } elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) {
46 // reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain
47 $ip = $this->getIpFromHost($query->name);
48
49 if ($ip !== null) {
50 $records = array();
51 foreach ($this->hosts->getHostsForIp($ip) as $host) {
52 $records[] = new Record($query->name, $query->type, $query->class, 0, $host);
53 }
54
55 if ($records) {
56 return Promise\resolve(
57 Message::createResponseWithAnswersForQuery($query, $records)
58 );
59 }
60 }
61 }
62
63 return $this->fallback->query($nameserver, $query);
64 }
65
66 private function getIpFromHost($host)
67 {
68 if (substr($host, -13) === '.in-addr.arpa') {
69 // IPv4: read as IP and reverse bytes
70 $ip = @inet_pton(substr($host, 0, -13));
71 if ($ip === false || isset($ip[4])) {
72 return null;
73 }
74
75 return inet_ntop(strrev($ip));
76 } elseif (substr($host, -9) === '.ip6.arpa') {
77 // IPv6: replace dots, reverse nibbles and interpret as hexadecimal string
78 $ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9)))));
79 if ($ip === false) {
80 return null;
81 }
82
83 return $ip;
84 } else {
85 return null;
86 }
87 }
88 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 class Query
5 {
6 public $name;
7 public $type;
8 public $class;
9 public $currentTime;
10
11 public function __construct($name, $type, $class, $currentTime)
12 {
13 $this->name = $name;
14 $this->type = $type;
15 $this->class = $class;
16 $this->currentTime = $currentTime;
17 }
18 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\Dns\Model\Message;
5 use React\Dns\Model\Record;
6
7 class RecordBag
8 {
9 private $records = array();
10
11 public function set($currentTime, Record $record)
12 {
13 $this->records[$record->data] = array($currentTime + $record->ttl, $record);
14 }
15
16 public function all()
17 {
18 return array_values(array_map(
19 function ($value) {
20 list($expiresAt, $record) = $value;
21 return $record;
22 },
23 $this->records
24 ));
25 }
26 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\Cache\CacheInterface;
5 use React\Dns\Model\Message;
6 use React\Dns\Model\Record;
7 use React\Promise;
8
9 class RecordCache
10 {
11 private $cache;
12 private $expiredAt;
13
14 public function __construct(CacheInterface $cache)
15 {
16 $this->cache = $cache;
17 }
18
19 public function lookup(Query $query)
20 {
21 $id = $this->serializeQueryToIdentity($query);
22
23 $expiredAt = $this->expiredAt;
24
25 return $this->cache
26 ->get($id)
27 ->then(function ($value) use ($query, $expiredAt) {
28 $recordBag = unserialize($value);
29
30 if (null !== $expiredAt && $expiredAt <= $query->currentTime) {
31 return Promise\reject();
32 }
33
34 return $recordBag->all();
35 });
36 }
37
38 public function storeResponseMessage($currentTime, Message $message)
39 {
40 foreach ($message->answers as $record) {
41 $this->storeRecord($currentTime, $record);
42 }
43 }
44
45 public function storeRecord($currentTime, Record $record)
46 {
47 $id = $this->serializeRecordToIdentity($record);
48
49 $cache = $this->cache;
50
51 $this->cache
52 ->get($id)
53 ->then(
54 function ($value) {
55 return unserialize($value);
56 },
57 function ($e) {
58 return new RecordBag();
59 }
60 )
61 ->then(function ($recordBag) use ($id, $currentTime, $record, $cache) {
62 $recordBag->set($currentTime, $record);
63 $cache->set($id, serialize($recordBag));
64 });
65 }
66
67 public function expire($currentTime)
68 {
69 $this->expiredAt = $currentTime;
70 }
71
72 public function serializeQueryToIdentity(Query $query)
73 {
74 return sprintf('%s:%s:%s', $query->name, $query->type, $query->class);
75 }
76
77 public function serializeRecordToIdentity(Record $record)
78 {
79 return sprintf('%s:%s:%s', $record->name, $record->type, $record->class);
80 }
81 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\Promise\Deferred;
5
6 class RetryExecutor implements ExecutorInterface
7 {
8 private $executor;
9 private $retries;
10
11 public function __construct(ExecutorInterface $executor, $retries = 2)
12 {
13 $this->executor = $executor;
14 $this->retries = $retries;
15 }
16
17 public function query($nameserver, Query $query)
18 {
19 return $this->tryQuery($nameserver, $query, $this->retries);
20 }
21
22 public function tryQuery($nameserver, Query $query, $retries)
23 {
24 $that = $this;
25 $errorback = function ($error) use ($nameserver, $query, $retries, $that) {
26 if (!$error instanceof TimeoutException) {
27 throw $error;
28 }
29 if (0 >= $retries) {
30 throw new \RuntimeException(
31 sprintf("DNS query for %s failed: too many retries", $query->name),
32 0,
33 $error
34 );
35 }
36 return $that->tryQuery($nameserver, $query, $retries-1);
37 };
38
39 return $this->executor
40 ->query($nameserver, $query)
41 ->then(null, $errorback);
42 }
43 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 class TimeoutException extends \Exception
5 {
6 }
0 <?php
1
2 namespace React\Dns\Query;
3
4 use React\EventLoop\LoopInterface;
5 use React\Promise\Deferred;
6 use React\Promise\CancellablePromiseInterface;
7 use React\Promise\Timer;
8
9 class TimeoutExecutor implements ExecutorInterface
10 {
11 private $executor;
12 private $loop;
13 private $timeout;
14
15 public function __construct(ExecutorInterface $executor, $timeout, LoopInterface $loop)
16 {
17 $this->executor = $executor;
18 $this->loop = $loop;
19 $this->timeout = $timeout;
20 }
21
22 public function query($nameserver, Query $query)
23 {
24 return Timer\timeout($this->executor->query($nameserver, $query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) {
25 if ($e instanceof Timer\TimeoutException) {
26 $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e);
27 }
28 throw $e;
29 });
30 }
31 }
0 <?php
1
2 namespace React\Dns;
3
4 class RecordNotFoundException extends \Exception
5 {
6 }
0 <?php
1
2 namespace React\Dns\Resolver;
3
4 use React\Cache\ArrayCache;
5 use React\Cache\CacheInterface;
6 use React\Dns\Config\HostsFile;
7 use React\Dns\Protocol\Parser;
8 use React\Dns\Protocol\BinaryDumper;
9 use React\Dns\Query\CachedExecutor;
10 use React\Dns\Query\Executor;
11 use React\Dns\Query\ExecutorInterface;
12 use React\Dns\Query\HostsFileExecutor;
13 use React\Dns\Query\RecordCache;
14 use React\Dns\Query\RetryExecutor;
15 use React\Dns\Query\TimeoutExecutor;
16 use React\EventLoop\LoopInterface;
17
18 class Factory
19 {
20 public function create($nameserver, LoopInterface $loop)
21 {
22 $nameserver = $this->addPortToServerIfMissing($nameserver);
23 $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop));
24
25 return new Resolver($nameserver, $executor);
26 }
27
28 public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null)
29 {
30 if (!($cache instanceof CacheInterface)) {
31 $cache = new ArrayCache();
32 }
33
34 $nameserver = $this->addPortToServerIfMissing($nameserver);
35 $executor = $this->decorateHostsFileExecutor($this->createCachedExecutor($loop, $cache));
36
37 return new Resolver($nameserver, $executor);
38 }
39
40 /**
41 * Tries to load the hosts file and decorates the given executor on success
42 *
43 * @param ExecutorInterface $executor
44 * @return ExecutorInterface
45 * @codeCoverageIgnore
46 */
47 private function decorateHostsFileExecutor(ExecutorInterface $executor)
48 {
49 try {
50 $executor = new HostsFileExecutor(
51 HostsFile::loadFromPathBlocking(),
52 $executor
53 );
54 } catch (\RuntimeException $e) {
55 // ignore this file if it can not be loaded
56 }
57
58 // Windows does not store localhost in hosts file by default but handles this internally
59 // To compensate for this, we explicitly use hard-coded defaults for localhost
60 if (DIRECTORY_SEPARATOR === '\\') {
61 $executor = new HostsFileExecutor(
62 new HostsFile("127.0.0.1 localhost\n::1 localhost"),
63 $executor
64 );
65 }
66
67 return $executor;
68 }
69
70 protected function createExecutor(LoopInterface $loop)
71 {
72 return new TimeoutExecutor(
73 new Executor($loop, new Parser(), new BinaryDumper(), null),
74 5.0,
75 $loop
76 );
77 }
78
79 protected function createRetryExecutor(LoopInterface $loop)
80 {
81 return new RetryExecutor($this->createExecutor($loop));
82 }
83
84 protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache)
85 {
86 return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache));
87 }
88
89 protected function addPortToServerIfMissing($nameserver)
90 {
91 if (strpos($nameserver, '[') === false && substr_count($nameserver, ':') >= 2) {
92 // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
93 $nameserver = '[' . $nameserver . ']';
94 }
95 // assume a dummy scheme when checking for the port, otherwise parse_url() fails
96 if (parse_url('dummy://' . $nameserver, PHP_URL_PORT) === null) {
97 $nameserver .= ':53';
98 }
99
100 return $nameserver;
101 }
102 }
0 <?php
1
2 namespace React\Dns\Resolver;
3
4 use React\Dns\Query\ExecutorInterface;
5 use React\Dns\Query\Query;
6 use React\Dns\RecordNotFoundException;
7 use React\Dns\Model\Message;
8
9 class Resolver
10 {
11 private $nameserver;
12 private $executor;
13
14 public function __construct($nameserver, ExecutorInterface $executor)
15 {
16 $this->nameserver = $nameserver;
17 $this->executor = $executor;
18 }
19
20 public function resolve($domain)
21 {
22 $query = new Query($domain, Message::TYPE_A, Message::CLASS_IN, time());
23 $that = $this;
24
25 return $this->executor
26 ->query($this->nameserver, $query)
27 ->then(function (Message $response) use ($query, $that) {
28 return $that->extractAddress($query, $response);
29 });
30 }
31
32 public function extractAddress(Query $query, Message $response)
33 {
34 $answers = $response->answers;
35
36 $addresses = $this->resolveAliases($answers, $query->name);
37
38 if (0 === count($addresses)) {
39 $message = 'DNS Request did not return valid answer.';
40 throw new RecordNotFoundException($message);
41 }
42
43 $address = $addresses[array_rand($addresses)];
44 return $address;
45 }
46
47 public function resolveAliases(array $answers, $name)
48 {
49 $named = $this->filterByName($answers, $name);
50 $aRecords = $this->filterByType($named, Message::TYPE_A);
51 $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
52
53 if ($aRecords) {
54 return $this->mapRecordData($aRecords);
55 }
56
57 if ($cnameRecords) {
58 $aRecords = array();
59
60 $cnames = $this->mapRecordData($cnameRecords);
61 foreach ($cnames as $cname) {
62 $targets = $this->filterByName($answers, $cname);
63 $aRecords = array_merge(
64 $aRecords,
65 $this->resolveAliases($answers, $cname)
66 );
67 }
68
69 return $aRecords;
70 }
71
72 return array();
73 }
74
75 private function filterByName(array $answers, $name)
76 {
77 return $this->filterByField($answers, 'name', $name);
78 }
79
80 private function filterByType(array $answers, $type)
81 {
82 return $this->filterByField($answers, 'type', $type);
83 }
84
85 private function filterByField(array $answers, $field, $value)
86 {
87 $value = strtolower($value);
88 return array_filter($answers, function ($answer) use ($field, $value) {
89 return $value === strtolower($answer->$field);
90 });
91 }
92
93 private function mapRecordData(array $records)
94 {
95 return array_map(function ($record) {
96 return $record->data;
97 }, $records);
98 }
99 }
0 <?php
1
2 namespace React\Tests\Dns;
3
4 class CallableStub
5 {
6 public function __invoke()
7 {
8 }
9 }
0 <?php
1
2 namespace React\Tests\Dns\Config;
3
4 use React\Tests\Dns\TestCase;
5 use React\Dns\Config\Config;
6
7 class ConfigTest extends TestCase
8 {
9 public function testLoadsSystemDefault()
10 {
11 $config = Config::loadSystemConfigBlocking();
12
13 $this->assertInstanceOf('React\Dns\Config\Config', $config);
14 }
15
16 public function testLoadsDefaultPath()
17 {
18 if (DIRECTORY_SEPARATOR === '\\') {
19 $this->markTestSkipped('Not supported on Windows');
20 }
21
22 $config = Config::loadResolvConfBlocking();
23
24 $this->assertInstanceOf('React\Dns\Config\Config', $config);
25 }
26
27 public function testLoadsFromExplicitPath()
28 {
29 $config = Config::loadResolvConfBlocking(__DIR__ . '/../Fixtures/etc/resolv.conf');
30
31 $this->assertEquals(array('8.8.8.8'), $config->nameservers);
32 }
33
34 /**
35 * @expectedException RuntimeException
36 */
37 public function testLoadThrowsWhenPathIsInvalid()
38 {
39 Config::loadResolvConfBlocking(__DIR__ . '/invalid.conf');
40 }
41
42 public function testParsesSingleEntryFile()
43 {
44 $contents = 'nameserver 8.8.8.8';
45 $expected = array('8.8.8.8');
46
47 $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents));
48 $this->assertEquals($expected, $config->nameservers);
49 }
50
51 public function testParsesNameserverEntriesFromAverageFileCorrectly()
52 {
53 $contents = '#
54 # Mac OS X Notice
55 #
56 # This file is not used by the host name and address resolution
57 # or the DNS query routing mechanisms used by most processes on
58 # this Mac OS X system.
59 #
60 # This file is automatically generated.
61 #
62 domain v.cablecom.net
63 nameserver 127.0.0.1
64 nameserver ::1
65 ';
66 $expected = array('127.0.0.1', '::1');
67
68 $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents));
69 $this->assertEquals($expected, $config->nameservers);
70 }
71
72 public function testParsesEmptyFileWithoutNameserverEntries()
73 {
74 $contents = '';
75 $expected = array();
76
77 $config = Config::loadResolvConfBlocking('data://text/plain;base64,');
78 $this->assertEquals($expected, $config->nameservers);
79 }
80
81 public function testParsesFileAndIgnoresCommentsAndInvalidNameserverEntries()
82 {
83 $contents = '
84 # nameserver 1.2.3.4
85 ; nameserver 2.3.4.5
86
87 nameserver 3.4.5.6 # nope
88 nameserver 4.5.6.7 5.6.7.8
89 nameserver 6.7.8.9
90 NameServer 7.8.9.10
91 ';
92 $expected = array();
93
94 $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents));
95 $this->assertEquals($expected, $config->nameservers);
96 }
97
98 public function testLoadsFromWmicOnWindows()
99 {
100 if (DIRECTORY_SEPARATOR !== '\\') {
101 $this->markTestSkipped('Only on Windows');
102 }
103
104 $config = Config::loadWmicBlocking();
105
106 $this->assertInstanceOf('React\Dns\Config\Config', $config);
107 }
108
109 public function testLoadsSingleEntryFromWmicOutput()
110 {
111 $contents = '
112 Node,DNSServerSearchOrder
113 ACE,
114 ACE,{192.168.2.1}
115 ACE,
116 ';
117 $expected = array('192.168.2.1');
118
119 $config = Config::loadWmicBlocking($this->echoCommand($contents));
120
121 $this->assertEquals($expected, $config->nameservers);
122 }
123
124 public function testLoadsEmptyListFromWmicOutput()
125 {
126 $contents = '
127 Node,DNSServerSearchOrder
128 ACE,
129 ';
130 $expected = array();
131
132 $config = Config::loadWmicBlocking($this->echoCommand($contents));
133
134 $this->assertEquals($expected, $config->nameservers);
135 }
136
137 public function testLoadsSingleEntryForMultipleNicsFromWmicOutput()
138 {
139 $contents = '
140 Node,DNSServerSearchOrder
141 ACE,
142 ACE,{192.168.2.1}
143 ACE,
144 ACE,{192.168.2.2}
145 ACE,
146 ';
147 $expected = array('192.168.2.1', '192.168.2.2');
148
149 $config = Config::loadWmicBlocking($this->echoCommand($contents));
150
151 $this->assertEquals($expected, $config->nameservers);
152 }
153
154 public function testLoadsMultipleEntriesForSingleNicWithSemicolonFromWmicOutput()
155 {
156 $contents = '
157 Node,DNSServerSearchOrder
158 ACE,
159 ACE,{192.168.2.1;192.168.2.2}
160 ACE,
161 ';
162 $expected = array('192.168.2.1', '192.168.2.2');
163
164 $config = Config::loadWmicBlocking($this->echoCommand($contents));
165
166 $this->assertEquals($expected, $config->nameservers);
167 }
168
169 public function testLoadsMultipleEntriesForSingleNicWithQuotesFromWmicOutput()
170 {
171 $contents = '
172 Node,DNSServerSearchOrder
173 ACE,
174 ACE,{"192.168.2.1","192.168.2.2"}
175 ACE,
176 ';
177 $expected = array('192.168.2.1', '192.168.2.2');
178
179 $config = Config::loadWmicBlocking($this->echoCommand($contents));
180
181 $this->assertEquals($expected, $config->nameservers);
182 }
183
184 private function echoCommand($output)
185 {
186 return 'echo ' . escapeshellarg($output);
187 }
188 }
0 <?php
1
2 namespace React\Test\Dns\Config;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Dns\Config\FilesystemFactory;
6
7 class FilesystemFactoryTest extends TestCase
8 {
9 /** @test */
10 public function parseEtcResolvConfShouldParseCorrectly()
11 {
12 $contents = '#
13 # Mac OS X Notice
14 #
15 # This file is not used by the host name and address resolution
16 # or the DNS query routing mechanisms used by most processes on
17 # this Mac OS X system.
18 #
19 # This file is automatically generated.
20 #
21 domain v.cablecom.net
22 nameserver 127.0.0.1
23 nameserver 8.8.8.8
24 ';
25 $expected = array('127.0.0.1', '8.8.8.8');
26
27 $capturedConfig = null;
28
29 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
30 $factory = new FilesystemFactory($loop);
31 $factory->parseEtcResolvConf($contents)->then(function ($config) use (&$capturedConfig) {
32 $capturedConfig = $config;
33 });
34
35 $this->assertNotNull($capturedConfig);
36 $this->assertSame($expected, $capturedConfig->nameservers);
37 }
38
39 /** @test */
40 public function createShouldLoadStuffFromFilesystem()
41 {
42 $this->markTestIncomplete('Filesystem API is incomplete');
43
44 $expected = array('8.8.8.8');
45
46 $triggerListener = null;
47 $capturedConfig = null;
48
49 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
50 $loop
51 ->expects($this->once())
52 ->method('addReadStream')
53 ->will($this->returnCallback(function ($stream, $listener) use (&$triggerListener) {
54 $triggerListener = function () use ($stream, $listener) {
55 call_user_func($listener, $stream);
56 };
57 }));
58
59 $factory = new FilesystemFactory($loop);
60 $factory->create(__DIR__.'/../Fixtures/etc/resolv.conf')->then(function ($config) use (&$capturedConfig) {
61 $capturedConfig = $config;
62 });
63
64 $triggerListener();
65
66 $this->assertNotNull($capturedConfig);
67 $this->assertSame($expected, $capturedConfig->nameservers);
68 }
69 }
0 <?php
1
2 namespace React\Tests\Dns\Config;
3
4 use React\Tests\Dns\TestCase;
5 use React\Dns\Config\HostsFile;
6
7 class HostsFileTest extends TestCase
8 {
9 public function testLoadsFromDefaultPath()
10 {
11 $hosts = HostsFile::loadFromPathBlocking();
12
13 $this->assertInstanceOf('React\Dns\Config\HostsFile', $hosts);
14 }
15
16 public function testDefaultShouldHaveLocalhostMapped()
17 {
18 if (DIRECTORY_SEPARATOR === '\\') {
19 $this->markTestSkipped('Not supported on Windows');
20 }
21
22 $hosts = HostsFile::loadFromPathBlocking();
23
24 $this->assertContains('127.0.0.1', $hosts->getIpsForHost('localhost'));
25 }
26
27 /**
28 * @expectedException RuntimeException
29 */
30 public function testLoadThrowsForInvalidPath()
31 {
32 HostsFile::loadFromPathBlocking('does/not/exist');
33 }
34
35 public function testContainsSingleLocalhostEntry()
36 {
37 $hosts = new HostsFile('127.0.0.1 localhost');
38
39 $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost'));
40 $this->assertEquals(array(), $hosts->getIpsForHost('example.com'));
41 }
42
43 public function testNonIpReturnsNothingForInvalidHosts()
44 {
45 $hosts = new HostsFile('a b');
46
47 $this->assertEquals(array(), $hosts->getIpsForHost('a'));
48 $this->assertEquals(array(), $hosts->getIpsForHost('b'));
49 }
50
51 public function testIgnoresIpv6ZoneId()
52 {
53 $hosts = new HostsFile('fe80::1%lo0 localhost');
54
55 $this->assertEquals(array('fe80::1'), $hosts->getIpsForHost('localhost'));
56 }
57
58 public function testSkipsComments()
59 {
60 $hosts = new HostsFile('# start' . PHP_EOL .'#127.0.0.1 localhost' . PHP_EOL . '127.0.0.2 localhost # example.com');
61
62 $this->assertEquals(array('127.0.0.2'), $hosts->getIpsForHost('localhost'));
63 $this->assertEquals(array(), $hosts->getIpsForHost('example.com'));
64 }
65
66 public function testContainsSingleLocalhostEntryWithCaseIgnored()
67 {
68 $hosts = new HostsFile('127.0.0.1 LocalHost');
69
70 $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('LOCALHOST'));
71 }
72
73 public function testEmptyFileContainsNothing()
74 {
75 $hosts = new HostsFile('');
76
77 $this->assertEquals(array(), $hosts->getIpsForHost('example.com'));
78 }
79
80 public function testSingleEntryWithMultipleNames()
81 {
82 $hosts = new HostsFile('127.0.0.1 localhost example.com');
83
84 $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('example.com'));
85 $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost'));
86 }
87
88 public function testMergesEntriesOverMultipleLines()
89 {
90 $hosts = new HostsFile("127.0.0.1 localhost\n127.0.0.2 localhost\n127.0.0.3 a localhost b\n127.0.0.4 a localhost");
91
92 $this->assertEquals(array('127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'), $hosts->getIpsForHost('localhost'));
93 }
94
95 public function testMergesIpv4AndIpv6EntriesOverMultipleLines()
96 {
97 $hosts = new HostsFile("127.0.0.1 localhost\n::1 localhost");
98
99 $this->assertEquals(array('127.0.0.1', '::1'), $hosts->getIpsForHost('localhost'));
100 }
101
102 public function testReverseLookup()
103 {
104 $hosts = new HostsFile('127.0.0.1 localhost');
105
106 $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1'));
107 $this->assertEquals(array(), $hosts->getHostsForIp('192.168.1.1'));
108 }
109
110 public function testReverseSkipsComments()
111 {
112 $hosts = new HostsFile("# start\n#127.0.0.1 localhosted\n127.0.0.2\tlocalhost\t# example.com\n\t127.0.0.3\t\texample.org\t\t");
113
114 $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1'));
115 $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.2'));
116 $this->assertEquals(array('example.org'), $hosts->getHostsForIp('127.0.0.3'));
117 }
118
119 public function testReverseNonIpReturnsNothing()
120 {
121 $hosts = new HostsFile('127.0.0.1 localhost');
122
123 $this->assertEquals(array(), $hosts->getHostsForIp('localhost'));
124 $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1.1'));
125 }
126
127 public function testReverseNonIpReturnsNothingForInvalidHosts()
128 {
129 $hosts = new HostsFile('a b');
130
131 $this->assertEquals(array(), $hosts->getHostsForIp('a'));
132 $this->assertEquals(array(), $hosts->getHostsForIp('b'));
133 }
134
135 public function testReverseLookupReturnsLowerCaseHost()
136 {
137 $hosts = new HostsFile('127.0.0.1 LocalHost');
138
139 $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1'));
140 }
141
142 public function testReverseLookupChecksNormalizedIpv6()
143 {
144 $hosts = new HostsFile('FE80::00a1 localhost');
145
146 $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::A1'));
147 }
148
149 public function testReverseLookupIgnoresIpv6ZoneId()
150 {
151 $hosts = new HostsFile('fe80::1%lo0 localhost');
152
153 $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::1'));
154 }
155
156 public function testReverseLookupReturnsMultipleHostsOverSingleLine()
157 {
158 $hosts = new HostsFile("::1 ip6-localhost ip6-loopback");
159
160 $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1'));
161 }
162
163 public function testReverseLookupReturnsMultipleHostsOverMultipleLines()
164 {
165 $hosts = new HostsFile("::1 ip6-localhost\n::1 ip6-loopback");
166
167 $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1'));
168 }
169 }
0 nameserver 8.8.8.8
0 <?php
1
2 namespace React\Tests\Dns;
3
4 use React\Tests\Dns\TestCase;
5 use React\EventLoop\Factory as LoopFactory;
6 use React\Dns\Resolver\Resolver;
7 use React\Dns\Resolver\Factory;
8
9 class FunctionalTest extends TestCase
10 {
11 public function setUp()
12 {
13 $this->loop = LoopFactory::create();
14
15 $factory = new Factory();
16 $this->resolver = $factory->create('8.8.8.8', $this->loop);
17 }
18
19 public function testResolveLocalhostResolves()
20 {
21 $promise = $this->resolver->resolve('localhost');
22 $promise->then($this->expectCallableOnce(), $this->expectCallableNever());
23
24 $this->loop->run();
25 }
26
27 /**
28 * @group internet
29 */
30 public function testResolveGoogleResolves()
31 {
32 $promise = $this->resolver->resolve('google.com');
33 $promise->then($this->expectCallableOnce(), $this->expectCallableNever());
34
35 $this->loop->run();
36 }
37
38 /**
39 * @group internet
40 */
41 public function testResolveInvalidRejects()
42 {
43 $promise = $this->resolver->resolve('example.invalid');
44 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
45
46 $this->loop->run();
47 }
48
49 public function testResolveCancelledRejectsImmediately()
50 {
51 $promise = $this->resolver->resolve('google.com');
52 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
53 $promise->cancel();
54
55 $time = microtime(true);
56 $this->loop->run();
57 $time = microtime(true) - $time;
58
59 $this->assertLessThan(0.1, $time);
60 }
61
62 public function testInvalidResolverDoesNotResolveGoogle()
63 {
64 $factory = new Factory();
65 $this->resolver = $factory->create('255.255.255.255', $this->loop);
66
67 $promise = $this->resolver->resolve('google.com');
68 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
69 }
70 }
0 <?php
1
2 namespace React\Tests\Dns\Model;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Dns\Query\Query;
6 use React\Dns\Model\Message;
7
8 class MessageTest extends TestCase
9 {
10 public function testCreateRequestDesiresRecusion()
11 {
12 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
13 $request = Message::createRequestForQuery($query);
14
15 $this->assertTrue($request->header->isQuery());
16 $this->assertSame(1, $request->header->get('rd'));
17 }
18
19 public function testCreateResponseWithNoAnswers()
20 {
21 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
22 $answers = array();
23 $request = Message::createResponseWithAnswersForQuery($query, $answers);
24
25 $this->assertFalse($request->header->isQuery());
26 $this->assertTrue($request->header->isResponse());
27 $this->assertEquals(0, $request->header->get('anCount'));
28 }
29 }
0 <?php
1
2 namespace React\Tests\Dns\Protocol;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Dns\Protocol\BinaryDumper;
6 use React\Dns\Model\Message;
7
8 class BinaryDumperTest extends TestCase
9 {
10 public function testRequestToBinary()
11 {
12 $data = "";
13 $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header
14 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
15 $data .= "00 01 00 01"; // question: type A, class IN
16
17 $expected = $this->formatHexDump(str_replace(' ', '', $data), 2);
18
19 $request = new Message();
20 $request->header->set('id', 0x7262);
21 $request->header->set('rd', 1);
22
23 $request->questions[] = array(
24 'name' => 'igor.io',
25 'type' => Message::TYPE_A,
26 'class' => Message::CLASS_IN,
27 );
28
29 $request->prepare();
30
31 $dumper = new BinaryDumper();
32 $data = $dumper->toBinary($request);
33 $data = $this->convertBinaryToHexDump($data);
34
35 $this->assertSame($expected, $data);
36 }
37
38 private function convertBinaryToHexDump($input)
39 {
40 return $this->formatHexDump(implode('', unpack('H*', $input)));
41 }
42
43 private function formatHexDump($input)
44 {
45 return implode(' ', str_split($input, 2));
46 }
47 }
0 <?php
1
2 namespace React\Tests\Dns\Protocol;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Dns\Protocol\Parser;
6 use React\Dns\Model\Message;
7
8 class ParserTest extends TestCase
9 {
10 public function setUp()
11 {
12 $this->parser = new Parser();
13 }
14
15 /**
16 * @dataProvider provideConvertTcpDumpToBinary
17 */
18 public function testConvertTcpDumpToBinary($expected, $data)
19 {
20 $this->assertSame($expected, $this->convertTcpDumpToBinary($data));
21 }
22
23 public function provideConvertTcpDumpToBinary()
24 {
25 return array(
26 array(chr(0x72).chr(0x62), "72 62"),
27 array(chr(0x72).chr(0x62).chr(0x01).chr(0x00), "72 62 01 00"),
28 array(chr(0x72).chr(0x62).chr(0x01).chr(0x00).chr(0x00).chr(0x01), "72 62 01 00 00 01"),
29 array(chr(0x01).chr(0x00).chr(0x01), "01 00 01"),
30 );
31 }
32
33 public function testParseRequest()
34 {
35 $data = "";
36 $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header
37 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
38 $data .= "00 01 00 01"; // question: type A, class IN
39
40 $data = $this->convertTcpDumpToBinary($data);
41
42 $request = $this->parser->parseMessage($data);
43
44 $header = $request->header;
45 $this->assertSame(0x7262, $header->get('id'));
46 $this->assertSame(1, $header->get('qdCount'));
47 $this->assertSame(0, $header->get('anCount'));
48 $this->assertSame(0, $header->get('nsCount'));
49 $this->assertSame(0, $header->get('arCount'));
50 $this->assertSame(0, $header->get('qr'));
51 $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode'));
52 $this->assertSame(0, $header->get('aa'));
53 $this->assertSame(0, $header->get('tc'));
54 $this->assertSame(1, $header->get('rd'));
55 $this->assertSame(0, $header->get('ra'));
56 $this->assertSame(0, $header->get('z'));
57 $this->assertSame(Message::RCODE_OK, $header->get('rcode'));
58
59 $this->assertCount(1, $request->questions);
60 $this->assertSame('igor.io', $request->questions[0]['name']);
61 $this->assertSame(Message::TYPE_A, $request->questions[0]['type']);
62 $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']);
63 }
64
65 public function testParseResponse()
66 {
67 $data = "";
68 $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header
69 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
70 $data .= "00 01 00 01"; // question: type A, class IN
71 $data .= "c0 0c"; // answer: offset pointer to igor.io
72 $data .= "00 01 00 01"; // answer: type A, class IN
73 $data .= "00 01 51 80"; // answer: ttl 86400
74 $data .= "00 04"; // answer: rdlength 4
75 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131
76
77 $data = $this->convertTcpDumpToBinary($data);
78
79 $response = $this->parser->parseMessage($data);
80
81 $header = $response->header;
82 $this->assertSame(0x7262, $header->get('id'));
83 $this->assertSame(1, $header->get('qdCount'));
84 $this->assertSame(1, $header->get('anCount'));
85 $this->assertSame(0, $header->get('nsCount'));
86 $this->assertSame(0, $header->get('arCount'));
87 $this->assertSame(1, $header->get('qr'));
88 $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode'));
89 $this->assertSame(0, $header->get('aa'));
90 $this->assertSame(0, $header->get('tc'));
91 $this->assertSame(1, $header->get('rd'));
92 $this->assertSame(1, $header->get('ra'));
93 $this->assertSame(0, $header->get('z'));
94 $this->assertSame(Message::RCODE_OK, $header->get('rcode'));
95
96 $this->assertCount(1, $response->questions);
97 $this->assertSame('igor.io', $response->questions[0]['name']);
98 $this->assertSame(Message::TYPE_A, $response->questions[0]['type']);
99 $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']);
100
101 $this->assertCount(1, $response->answers);
102 $this->assertSame('igor.io', $response->answers[0]->name);
103 $this->assertSame(Message::TYPE_A, $response->answers[0]->type);
104 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
105 $this->assertSame(86400, $response->answers[0]->ttl);
106 $this->assertSame('178.79.169.131', $response->answers[0]->data);
107 }
108
109 public function testParseQuestionWithTwoQuestions()
110 {
111 $data = "";
112 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
113 $data .= "00 01 00 01"; // question: type A, class IN
114 $data .= "03 77 77 77 04 69 67 6f 72 02 69 6f 00"; // question: www.igor.io
115 $data .= "00 01 00 01"; // question: type A, class IN
116
117 $data = $this->convertTcpDumpToBinary($data);
118
119 $request = new Message();
120 $request->header->set('qdCount', 2);
121 $request->data = $data;
122
123 $this->parser->parseQuestion($request);
124
125 $this->assertCount(2, $request->questions);
126 $this->assertSame('igor.io', $request->questions[0]['name']);
127 $this->assertSame(Message::TYPE_A, $request->questions[0]['type']);
128 $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']);
129 $this->assertSame('www.igor.io', $request->questions[1]['name']);
130 $this->assertSame(Message::TYPE_A, $request->questions[1]['type']);
131 $this->assertSame(Message::CLASS_IN, $request->questions[1]['class']);
132 }
133
134 public function testParseAnswerWithInlineData()
135 {
136 $data = "";
137 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
138 $data .= "00 01 00 01"; // answer: type A, class IN
139 $data .= "00 01 51 80"; // answer: ttl 86400
140 $data .= "00 04"; // answer: rdlength 4
141 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131
142
143 $data = $this->convertTcpDumpToBinary($data);
144
145 $response = new Message();
146 $response->header->set('anCount', 1);
147 $response->data = $data;
148
149 $this->parser->parseAnswer($response);
150
151 $this->assertCount(1, $response->answers);
152 $this->assertSame('igor.io', $response->answers[0]->name);
153 $this->assertSame(Message::TYPE_A, $response->answers[0]->type);
154 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
155 $this->assertSame(86400, $response->answers[0]->ttl);
156 $this->assertSame('178.79.169.131', $response->answers[0]->data);
157 }
158
159 public function testParseResponseWithCnameAndOffsetPointers()
160 {
161 $data = "";
162 $data .= "9e 8d 81 80 00 01 00 01 00 00 00 00"; // header
163 $data .= "04 6d 61 69 6c 06 67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: mail.google.com
164 $data .= "00 05 00 01"; // question: type CNAME, class IN
165 $data .= "c0 0c"; // answer: offset pointer to mail.google.com
166 $data .= "00 05 00 01"; // answer: type CNAME, class IN
167 $data .= "00 00 a8 9c"; // answer: ttl 43164
168 $data .= "00 0f"; // answer: rdlength 15
169 $data .= "0a 67 6f 6f 67 6c 65 6d 61 69 6c 01 6c"; // answer: rdata googlemail.l.
170 $data .= "c0 11"; // answer: rdata offset pointer to google.com
171
172 $data = $this->convertTcpDumpToBinary($data);
173
174 $response = $this->parser->parseMessage($data);
175
176 $this->assertCount(1, $response->questions);
177 $this->assertSame('mail.google.com', $response->questions[0]['name']);
178 $this->assertSame(Message::TYPE_CNAME, $response->questions[0]['type']);
179 $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']);
180
181 $this->assertCount(1, $response->answers);
182 $this->assertSame('mail.google.com', $response->answers[0]->name);
183 $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type);
184 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
185 $this->assertSame(43164, $response->answers[0]->ttl);
186 $this->assertSame('googlemail.l.google.com', $response->answers[0]->data);
187 }
188
189 public function testParseAAAAResponse()
190 {
191 $data = "";
192 $data .= "cd 72 81 80 00 01 00 01 00 00 00 00 06"; // header
193 $data .= "67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: google.com
194 $data .= "00 1c 00 01"; // question: type AAAA, class IN
195 $data .= "c0 0c"; // answer: offset pointer to google.com
196 $data .= "00 1c 00 01"; // answer: type AAAA, class IN
197 $data .= "00 00 01 2b"; // answer: ttl 299
198 $data .= "00 10"; // answer: rdlength 16
199 $data .= "2a 00 14 50 40 09 08 09 00 00 00 00 00 00 20 0e"; // answer: 2a00:1450:4009:809::200e
200
201 $data = $this->convertTcpDumpToBinary($data);
202
203 $response = $this->parser->parseMessage($data);
204
205 $header = $response->header;
206 $this->assertSame(0xcd72, $header->get('id'));
207 $this->assertSame(1, $header->get('qdCount'));
208 $this->assertSame(1, $header->get('anCount'));
209 $this->assertSame(0, $header->get('nsCount'));
210 $this->assertSame(0, $header->get('arCount'));
211 $this->assertSame(1, $header->get('qr'));
212 $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode'));
213 $this->assertSame(0, $header->get('aa'));
214 $this->assertSame(0, $header->get('tc'));
215 $this->assertSame(1, $header->get('rd'));
216 $this->assertSame(1, $header->get('ra'));
217 $this->assertSame(0, $header->get('z'));
218 $this->assertSame(Message::RCODE_OK, $header->get('rcode'));
219
220 $this->assertCount(1, $response->questions);
221 $this->assertSame('google.com', $response->questions[0]['name']);
222 $this->assertSame(Message::TYPE_AAAA, $response->questions[0]['type']);
223 $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']);
224
225 $this->assertCount(1, $response->answers);
226 $this->assertSame('google.com', $response->answers[0]->name);
227 $this->assertSame(Message::TYPE_AAAA, $response->answers[0]->type);
228 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
229 $this->assertSame(299, $response->answers[0]->ttl);
230 $this->assertSame('2a00:1450:4009:809::200e', $response->answers[0]->data);
231 }
232
233 public function testParseResponseWithTwoAnswers()
234 {
235 $data = "";
236 $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header
237 $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00";
238 // question: io.whois-servers.net
239 $data .= "00 01 00 01"; // question: type A, class IN
240 $data .= "c0 0c"; // answer: offset pointer to io.whois-servers.net
241 $data .= "00 05 00 01"; // answer: type CNAME, class IN
242 $data .= "00 00 00 29"; // answer: ttl 41
243 $data .= "00 0e"; // answer: rdlength 14
244 $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // answer: rdata whois.nic.io
245 $data .= "c0 32"; // answer: offset pointer to whois.nic.io
246 $data .= "00 01 00 01"; // answer: type CNAME, class IN
247 $data .= "00 00 0d f7"; // answer: ttl 3575
248 $data .= "00 04"; // answer: rdlength 4
249 $data .= "c1 df 4e 98"; // answer: rdata 193.223.78.152
250
251 $data = $this->convertTcpDumpToBinary($data);
252
253 $response = $this->parser->parseMessage($data);
254
255 $this->assertCount(1, $response->questions);
256 $this->assertSame('io.whois-servers.net', $response->questions[0]['name']);
257 $this->assertSame(Message::TYPE_A, $response->questions[0]['type']);
258 $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']);
259
260 $this->assertCount(2, $response->answers);
261
262 $this->assertSame('io.whois-servers.net', $response->answers[0]->name);
263 $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type);
264 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
265 $this->assertSame(41, $response->answers[0]->ttl);
266 $this->assertSame('whois.nic.io', $response->answers[0]->data);
267
268 $this->assertSame('whois.nic.io', $response->answers[1]->name);
269 $this->assertSame(Message::TYPE_A, $response->answers[1]->type);
270 $this->assertSame(Message::CLASS_IN, $response->answers[1]->class);
271 $this->assertSame(3575, $response->answers[1]->ttl);
272 $this->assertSame('193.223.78.152', $response->answers[1]->data);
273 }
274
275 public function testParsePTRResponse()
276 {
277 $data = "";
278 $data .= "5d d8 81 80 00 01 00 01 00 00 00 00"; // header
279 $data .= "01 34 01 34 01 38 01 38 07 69 6e"; // question: 4.4.8.8.in-addr.arpa
280 $data .= "2d 61 64 64 72 04 61 72 70 61 00"; // question (continued)
281 $data .= "00 0c 00 01"; // question: type PTR, class IN
282 $data .= "c0 0c"; // answer: offset pointer to rdata
283 $data .= "00 0c 00 01"; // answer: type PTR, class IN
284 $data .= "00 01 51 7f"; // answer: ttl 86399
285 $data .= "00 20"; // answer: rdlength 32
286 $data .= "13 67 6f 6f 67 6c 65 2d 70 75 62 6c 69 63 2d 64"; // answer: rdata google-public-dns-b.google.com.
287 $data .= "6e 73 2d 62 06 67 6f 6f 67 6c 65 03 63 6f 6d 00";
288
289 $data = $this->convertTcpDumpToBinary($data);
290
291 $response = $this->parser->parseMessage($data);
292
293 $header = $response->header;
294 $this->assertSame(0x5dd8, $header->get('id'));
295 $this->assertSame(1, $header->get('qdCount'));
296 $this->assertSame(1, $header->get('anCount'));
297 $this->assertSame(0, $header->get('nsCount'));
298 $this->assertSame(0, $header->get('arCount'));
299 $this->assertSame(1, $header->get('qr'));
300 $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode'));
301 $this->assertSame(0, $header->get('aa'));
302 $this->assertSame(0, $header->get('tc'));
303 $this->assertSame(1, $header->get('rd'));
304 $this->assertSame(1, $header->get('ra'));
305 $this->assertSame(0, $header->get('z'));
306 $this->assertSame(Message::RCODE_OK, $header->get('rcode'));
307
308 $this->assertCount(1, $response->questions);
309 $this->assertSame('4.4.8.8.in-addr.arpa', $response->questions[0]['name']);
310 $this->assertSame(Message::TYPE_PTR, $response->questions[0]['type']);
311 $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']);
312
313 $this->assertCount(1, $response->answers);
314 $this->assertSame('4.4.8.8.in-addr.arpa', $response->answers[0]->name);
315 $this->assertSame(Message::TYPE_PTR, $response->answers[0]->type);
316 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
317 $this->assertSame(86399, $response->answers[0]->ttl);
318 $this->assertSame('google-public-dns-b.google.com', $response->answers[0]->data);
319 }
320
321 /**
322 * @expectedException InvalidArgumentException
323 */
324 public function testParseIncomplete()
325 {
326 $data = "";
327 $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header
328 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
329 //$data .= "00 01 00 01"; // question: type A, class IN
330
331 $data = $this->convertTcpDumpToBinary($data);
332
333 $this->parser->parseMessage($data);
334 }
335
336 private function convertTcpDumpToBinary($input)
337 {
338 // sudo ngrep -d en1 -x port 53
339
340 return pack('H*', str_replace(' ', '', $input));
341 }
342 }
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use React\Tests\Dns\TestCase;
5 use React\Dns\Query\CachedExecutor;
6 use React\Dns\Query\Query;
7 use React\Dns\Model\Message;
8 use React\Dns\Model\Record;
9 use React\Promise;
10
11 class CachedExecutorTest extends TestCase
12 {
13 /**
14 * @covers React\Dns\Query\CachedExecutor
15 * @test
16 */
17 public function queryShouldDelegateToDecoratedExecutor()
18 {
19 $executor = $this->createExecutorMock();
20 $executor
21 ->expects($this->once())
22 ->method('query')
23 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
24 ->will($this->returnValue($this->createPromiseMock()));
25
26 $cache = $this->getMockBuilder('React\Dns\Query\RecordCache')
27 ->disableOriginalConstructor()
28 ->getMock();
29 $cache
30 ->expects($this->once())
31 ->method('lookup')
32 ->will($this->returnValue(Promise\reject()));
33 $cachedExecutor = new CachedExecutor($executor, $cache);
34
35 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
36 $cachedExecutor->query('8.8.8.8', $query);
37 }
38
39 /**
40 * @covers React\Dns\Query\CachedExecutor
41 * @test
42 */
43 public function callingQueryTwiceShouldUseCachedResult()
44 {
45 $cachedRecords = array(new Record('igor.io', Message::TYPE_A, Message::CLASS_IN));
46
47 $executor = $this->createExecutorMock();
48 $executor
49 ->expects($this->once())
50 ->method('query')
51 ->will($this->callQueryCallbackWithAddress('178.79.169.131'));
52
53 $cache = $this->getMockBuilder('React\Dns\Query\RecordCache')
54 ->disableOriginalConstructor()
55 ->getMock();
56 $cache
57 ->expects($this->at(0))
58 ->method('lookup')
59 ->with($this->isInstanceOf('React\Dns\Query\Query'))
60 ->will($this->returnValue(Promise\reject()));
61 $cache
62 ->expects($this->at(1))
63 ->method('storeResponseMessage')
64 ->with($this->isType('integer'), $this->isInstanceOf('React\Dns\Model\Message'));
65 $cache
66 ->expects($this->at(2))
67 ->method('lookup')
68 ->with($this->isInstanceOf('React\Dns\Query\Query'))
69 ->will($this->returnValue(Promise\resolve($cachedRecords)));
70
71 $cachedExecutor = new CachedExecutor($executor, $cache);
72
73 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
74 $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {});
75 $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {});
76 }
77
78 private function callQueryCallbackWithAddress($address)
79 {
80 return $this->returnCallback(function ($nameserver, $query) use ($address) {
81 $response = new Message();
82 $response->header->set('qr', 1);
83 $response->questions[] = new Record($query->name, $query->type, $query->class);
84 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, $address);
85
86 return Promise\resolve($response);
87 });
88 }
89
90 private function createExecutorMock()
91 {
92 return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
93 }
94
95 private function createPromiseMock()
96 {
97 return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
98 }
99 }
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use Clue\React\Block;
5 use React\Dns\Query\Executor;
6 use React\Dns\Query\Query;
7 use React\Dns\Model\Message;
8 use React\Dns\Model\Record;
9 use React\Dns\Protocol\BinaryDumper;
10 use React\Tests\Dns\TestCase;
11
12 class ExecutorTest extends TestCase
13 {
14 private $loop;
15 private $parser;
16 private $dumper;
17 private $executor;
18
19 public function setUp()
20 {
21 $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
22 $this->parser = $this->getMockBuilder('React\Dns\Protocol\Parser')->getMock();
23 $this->dumper = new BinaryDumper();
24
25 $this->executor = new Executor($this->loop, $this->parser, $this->dumper);
26 }
27
28 /** @test */
29 public function queryShouldCreateUdpRequest()
30 {
31 $timer = $this->createTimerMock();
32 $this->loop
33 ->expects($this->any())
34 ->method('addTimer')
35 ->will($this->returnValue($timer));
36
37 $this->executor = $this->createExecutorMock();
38 $this->executor
39 ->expects($this->once())
40 ->method('createConnection')
41 ->with('8.8.8.8:53', 'udp')
42 ->will($this->returnNewConnectionMock(false));
43
44 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
45 $this->executor->query('8.8.8.8:53', $query);
46 }
47
48 /** @test */
49 public function resolveShouldRejectIfRequestIsLargerThan512Bytes()
50 {
51 $query = new Query(str_repeat('a', 512).'.igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
52 $promise = $this->executor->query('8.8.8.8:53', $query);
53
54 $this->setExpectedException('RuntimeException', 'DNS query for ' . $query->name . ' failed: Requested transport "tcp" not available, only UDP is supported in this version');
55 Block\await($promise, $this->loop);
56 }
57
58 /** @test */
59 public function resolveShouldCloseConnectionWhenCancelled()
60 {
61 $conn = $this->createConnectionMock(false);
62 $conn->expects($this->once())->method('close');
63
64 $timer = $this->createTimerMock();
65 $this->loop
66 ->expects($this->any())
67 ->method('addTimer')
68 ->will($this->returnValue($timer));
69
70 $this->executor = $this->createExecutorMock();
71 $this->executor
72 ->expects($this->once())
73 ->method('createConnection')
74 ->with('8.8.8.8:53', 'udp')
75 ->will($this->returnValue($conn));
76
77 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
78 $promise = $this->executor->query('8.8.8.8:53', $query);
79
80 $promise->cancel();
81
82 $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled');
83 Block\await($promise, $this->loop);
84 }
85
86 /** @test */
87 public function resolveShouldNotStartOrCancelTimerWhenCancelledWithTimeoutIsNull()
88 {
89 $this->loop
90 ->expects($this->never())
91 ->method('addTimer');
92
93 $this->executor = new Executor($this->loop, $this->parser, $this->dumper, null);
94
95 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
96 $promise = $this->executor->query('127.0.0.1:53', $query);
97
98 $promise->cancel();
99
100 $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled');
101 Block\await($promise, $this->loop);
102 }
103
104 /** @test */
105 public function resolveShouldRejectIfResponseIsTruncated()
106 {
107 $timer = $this->createTimerMock();
108
109 $this->loop
110 ->expects($this->any())
111 ->method('addTimer')
112 ->will($this->returnValue($timer));
113
114 $this->parser
115 ->expects($this->once())
116 ->method('parseMessage')
117 ->will($this->returnTruncatedResponse());
118
119 $this->executor = $this->createExecutorMock();
120 $this->executor
121 ->expects($this->once())
122 ->method('createConnection')
123 ->with('8.8.8.8:53', 'udp')
124 ->will($this->returnNewConnectionMock());
125
126 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
127 $this->executor->query('8.8.8.8:53', $query);
128 }
129
130 /** @test */
131 public function resolveShouldFailIfUdpThrow()
132 {
133 $this->loop
134 ->expects($this->never())
135 ->method('addTimer');
136
137 $this->parser
138 ->expects($this->never())
139 ->method('parseMessage');
140
141 $this->executor = $this->createExecutorMock();
142 $this->executor
143 ->expects($this->once())
144 ->method('createConnection')
145 ->with('8.8.8.8:53', 'udp')
146 ->will($this->throwException(new \Exception('Nope')));
147
148 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
149 $promise = $this->executor->query('8.8.8.8:53', $query);
150
151 $this->setExpectedException('RuntimeException', 'DNS query for igor.io failed: Nope');
152 Block\await($promise, $this->loop);
153 }
154
155 /** @test */
156 public function resolveShouldCancelTimerWhenFullResponseIsReceived()
157 {
158 $conn = $this->createConnectionMock();
159
160 $this->parser
161 ->expects($this->once())
162 ->method('parseMessage')
163 ->will($this->returnStandardResponse());
164
165 $this->executor = $this->createExecutorMock();
166 $this->executor
167 ->expects($this->at(0))
168 ->method('createConnection')
169 ->with('8.8.8.8:53', 'udp')
170 ->will($this->returnNewConnectionMock());
171
172
173 $timer = $this->createTimerMock();
174
175 $this->loop
176 ->expects($this->once())
177 ->method('addTimer')
178 ->with(5, $this->isInstanceOf('Closure'))
179 ->will($this->returnValue($timer));
180
181 $this->loop
182 ->expects($this->once())
183 ->method('cancelTimer')
184 ->with($timer);
185
186 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
187 $this->executor->query('8.8.8.8:53', $query);
188 }
189
190 /** @test */
191 public function resolveShouldCloseConnectionOnTimeout()
192 {
193 $this->executor = $this->createExecutorMock();
194 $this->executor
195 ->expects($this->at(0))
196 ->method('createConnection')
197 ->with('8.8.8.8:53', 'udp')
198 ->will($this->returnNewConnectionMock(false));
199
200 $timer = $this->createTimerMock();
201
202 $this->loop
203 ->expects($this->never())
204 ->method('cancelTimer');
205
206 $this->loop
207 ->expects($this->once())
208 ->method('addTimer')
209 ->with(5, $this->isInstanceOf('Closure'))
210 ->will($this->returnCallback(function ($time, $callback) use (&$timerCallback, $timer) {
211 $timerCallback = $callback;
212 return $timer;
213 }));
214
215 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
216 $promise = $this->executor->query('8.8.8.8:53', $query);
217
218 $this->assertNotNull($timerCallback);
219 $timerCallback();
220
221 $this->setExpectedException('React\Dns\Query\TimeoutException', 'DNS query for igor.io timed out');
222 Block\await($promise, $this->loop);
223 }
224
225 private function returnStandardResponse()
226 {
227 $that = $this;
228 $callback = function ($data) use ($that) {
229 $response = new Message();
230 $that->convertMessageToStandardResponse($response);
231 return $response;
232 };
233
234 return $this->returnCallback($callback);
235 }
236
237 private function returnTruncatedResponse()
238 {
239 $that = $this;
240 $callback = function ($data) use ($that) {
241 $response = new Message();
242 $that->convertMessageToTruncatedResponse($response);
243 return $response;
244 };
245
246 return $this->returnCallback($callback);
247 }
248
249 public function convertMessageToStandardResponse(Message $response)
250 {
251 $response->header->set('qr', 1);
252 $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN);
253 $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131');
254 $response->prepare();
255
256 return $response;
257 }
258
259 public function convertMessageToTruncatedResponse(Message $response)
260 {
261 $this->convertMessageToStandardResponse($response);
262 $response->header->set('tc', 1);
263 $response->prepare();
264
265 return $response;
266 }
267
268 private function returnNewConnectionMock($emitData = true)
269 {
270 $conn = $this->createConnectionMock($emitData);
271
272 $callback = function () use ($conn) {
273 return $conn;
274 };
275
276 return $this->returnCallback($callback);
277 }
278
279 private function createConnectionMock($emitData = true)
280 {
281 $conn = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock();
282 $conn
283 ->expects($this->any())
284 ->method('on')
285 ->with('data', $this->isInstanceOf('Closure'))
286 ->will($this->returnCallback(function ($name, $callback) use ($emitData) {
287 $emitData && $callback(null);
288 }));
289
290 return $conn;
291 }
292
293 private function createTimerMock()
294 {
295 return $this->getMockBuilder(
296 interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface'
297 )->getMock();
298 }
299
300 private function createExecutorMock()
301 {
302 return $this->getMockBuilder('React\Dns\Query\Executor')
303 ->setConstructorArgs(array($this->loop, $this->parser, $this->dumper))
304 ->setMethods(array('createConnection'))
305 ->getMock();
306 }
307 }
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use React\Tests\Dns\TestCase;
5 use React\Dns\Query\HostsFileExecutor;
6 use React\Dns\Query\Query;
7 use React\Dns\Model\Message;
8
9 class HostsFileExecutorTest extends TestCase
10 {
11 private $hosts;
12 private $fallback;
13 private $executor;
14
15 public function setUp()
16 {
17 $this->hosts = $this->getMockBuilder('React\Dns\Config\HostsFile')->disableOriginalConstructor()->getMock();
18 $this->fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
19 $this->executor = new HostsFileExecutor($this->hosts, $this->fallback);
20 }
21
22 public function testDoesNotTryToGetIpsForMxQuery()
23 {
24 $this->hosts->expects($this->never())->method('getIpsForHost');
25 $this->fallback->expects($this->once())->method('query');
26
27 $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_MX, Message::CLASS_IN, 0));
28 }
29
30 public function testFallsBackIfNoIpsWereFound()
31 {
32 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array());
33 $this->fallback->expects($this->once())->method('query');
34
35 $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0));
36 }
37
38 public function testReturnsResponseMessageIfIpsWereFound()
39 {
40 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1'));
41 $this->fallback->expects($this->never())->method('query');
42
43 $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0));
44 }
45
46 public function testFallsBackIfNoIpv4Matches()
47 {
48 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1'));
49 $this->fallback->expects($this->once())->method('query');
50
51 $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0));
52 }
53
54 public function testReturnsResponseMessageIfIpv6AddressesWereFound()
55 {
56 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1'));
57 $this->fallback->expects($this->never())->method('query');
58
59 $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0));
60 }
61
62 public function testFallsBackIfNoIpv6Matches()
63 {
64 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1'));
65 $this->fallback->expects($this->once())->method('query');
66
67 $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0));
68 }
69
70 public function testDoesReturnReverseIpv4Lookup()
71 {
72 $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array('localhost'));
73 $this->fallback->expects($this->never())->method('query');
74
75 $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0));
76 }
77
78 public function testFallsBackIfNoReverseIpv4Matches()
79 {
80 $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array());
81 $this->fallback->expects($this->once())->method('query');
82
83 $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0));
84 }
85
86 public function testDoesReturnReverseIpv6Lookup()
87 {
88 $this->hosts->expects($this->once())->method('getHostsForIp')->with('2a02:2e0:3fe:100::6')->willReturn(array('ip6-localhost'));
89 $this->fallback->expects($this->never())->method('query');
90
91 $this->executor->query('8.8.8.8', new Query('6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.e.f.3.0.0.e.2.0.2.0.a.2.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0));
92 }
93
94 public function testFallsBackForInvalidAddress()
95 {
96 $this->hosts->expects($this->never())->method('getHostsForIp');
97 $this->fallback->expects($this->once())->method('query');
98
99 $this->executor->query('8.8.8.8', new Query('example.com', Message::TYPE_PTR, Message::CLASS_IN, 0));
100 }
101
102 public function testReverseFallsBackForInvalidIpv4Address()
103 {
104 $this->hosts->expects($this->never())->method('getHostsForIp');
105 $this->fallback->expects($this->once())->method('query');
106
107 $this->executor->query('8.8.8.8', new Query('::1.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0));
108 }
109
110 public function testReverseFallsBackForInvalidLengthIpv6Address()
111 {
112 $this->hosts->expects($this->never())->method('getHostsForIp');
113 $this->fallback->expects($this->once())->method('query');
114
115 $this->executor->query('8.8.8.8', new Query('abcd.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0));
116 }
117
118 public function testReverseFallsBackForInvalidHexIpv6Address()
119 {
120 $this->hosts->expects($this->never())->method('getHostsForIp');
121 $this->fallback->expects($this->once())->method('query');
122
123 $this->executor->query('8.8.8.8', new Query('zZz.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0));
124 }
125 }
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Dns\Query\RecordBag;
6 use React\Dns\Model\Message;
7 use React\Dns\Model\Record;
8
9 class RecordBagTest extends TestCase
10 {
11 /**
12 * @covers React\Dns\Query\RecordBag
13 * @test
14 */
15 public function emptyBagShouldBeEmpty()
16 {
17 $recordBag = new RecordBag();
18
19 $this->assertSame(array(), $recordBag->all());
20 }
21
22 /**
23 * @covers React\Dns\Query\RecordBag
24 * @test
25 */
26 public function setShouldSetTheValue()
27 {
28 $currentTime = 1345656451;
29
30 $recordBag = new RecordBag();
31 $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600));
32
33 $records = $recordBag->all();
34 $this->assertCount(1, $records);
35 $this->assertSame('igor.io', $records[0]->name);
36 $this->assertSame(Message::TYPE_A, $records[0]->type);
37 $this->assertSame(Message::CLASS_IN, $records[0]->class);
38 }
39
40 /**
41 * @covers React\Dns\Query\RecordBag
42 * @test
43 */
44 public function setShouldSetManyValues()
45 {
46 $currentTime = 1345656451;
47
48 $recordBag = new RecordBag();
49 $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
50 $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'));
51
52 $records = $recordBag->all();
53 $this->assertCount(2, $records);
54 $this->assertSame('igor.io', $records[0]->name);
55 $this->assertSame(Message::TYPE_A, $records[0]->type);
56 $this->assertSame(Message::CLASS_IN, $records[0]->class);
57 $this->assertSame('178.79.169.131', $records[0]->data);
58 $this->assertSame('igor.io', $records[1]->name);
59 $this->assertSame(Message::TYPE_A, $records[1]->type);
60 $this->assertSame(Message::CLASS_IN, $records[1]->class);
61 $this->assertSame('178.79.169.132', $records[1]->data);
62 }
63 }
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Cache\ArrayCache;
6 use React\Dns\Model\Message;
7 use React\Dns\Model\Record;
8 use React\Dns\Query\RecordCache;
9 use React\Dns\Query\Query;
10 use React\Promise\PromiseInterface;
11
12 class RecordCacheTest extends TestCase
13 {
14 /**
15 * @covers React\Dns\Query\RecordCache
16 * @test
17 */
18 public function lookupOnEmptyCacheShouldReturnNull()
19 {
20 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
21
22 $cache = new RecordCache(new ArrayCache());
23 $promise = $cache->lookup($query);
24
25 $this->assertInstanceOf('React\Promise\RejectedPromise', $promise);
26 }
27
28 /**
29 * @covers React\Dns\Query\RecordCache
30 * @test
31 */
32 public function storeRecordShouldMakeLookupSucceed()
33 {
34 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
35
36 $cache = new RecordCache(new ArrayCache());
37 $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
38 $promise = $cache->lookup($query);
39
40 $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise);
41 $cachedRecords = $this->getPromiseValue($promise);
42
43 $this->assertCount(1, $cachedRecords);
44 $this->assertSame('178.79.169.131', $cachedRecords[0]->data);
45 }
46
47 /**
48 * @covers React\Dns\Query\RecordCache
49 * @test
50 */
51 public function storeTwoRecordsShouldReturnBoth()
52 {
53 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
54
55 $cache = new RecordCache(new ArrayCache());
56 $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
57 $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'));
58 $promise = $cache->lookup($query);
59
60 $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise);
61 $cachedRecords = $this->getPromiseValue($promise);
62
63 $this->assertCount(2, $cachedRecords);
64 $this->assertSame('178.79.169.131', $cachedRecords[0]->data);
65 $this->assertSame('178.79.169.132', $cachedRecords[1]->data);
66 }
67
68 /**
69 * @covers React\Dns\Query\RecordCache
70 * @test
71 */
72 public function storeResponseMessageShouldStoreAllAnswerValues()
73 {
74 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
75
76 $response = new Message();
77 $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131');
78 $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132');
79 $response->prepare();
80
81 $cache = new RecordCache(new ArrayCache());
82 $cache->storeResponseMessage($query->currentTime, $response);
83 $promise = $cache->lookup($query);
84
85 $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise);
86 $cachedRecords = $this->getPromiseValue($promise);
87
88 $this->assertCount(2, $cachedRecords);
89 $this->assertSame('178.79.169.131', $cachedRecords[0]->data);
90 $this->assertSame('178.79.169.132', $cachedRecords[1]->data);
91 }
92
93 /**
94 * @covers React\Dns\Query\RecordCache
95 * @test
96 */
97 public function expireShouldExpireDeadRecords()
98 {
99 $cachedTime = 1345656451;
100 $currentTime = $cachedTime + 3605;
101
102 $cache = new RecordCache(new ArrayCache());
103 $cache->storeRecord($cachedTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'));
104 $cache->expire($currentTime);
105
106 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, $currentTime);
107 $promise = $cache->lookup($query);
108
109 $this->assertInstanceOf('React\Promise\RejectedPromise', $promise);
110 }
111
112 private function getPromiseValue(PromiseInterface $promise)
113 {
114 $capturedValue = null;
115
116 $promise->then(function ($value) use (&$capturedValue) {
117 $capturedValue = $value;
118 });
119
120 return $capturedValue;
121 }
122 }
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use React\Tests\Dns\TestCase;
5 use React\Dns\Query\RetryExecutor;
6 use React\Dns\Query\Query;
7 use React\Dns\Model\Message;
8 use React\Dns\Query\TimeoutException;
9 use React\Dns\Model\Record;
10 use React\Promise;
11 use React\Promise\Deferred;
12 use React\Dns\Query\CancellationException;
13
14 class RetryExecutorTest extends TestCase
15 {
16 /**
17 * @covers React\Dns\Query\RetryExecutor
18 * @test
19 */
20 public function queryShouldDelegateToDecoratedExecutor()
21 {
22 $executor = $this->createExecutorMock();
23 $executor
24 ->expects($this->once())
25 ->method('query')
26 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
27 ->will($this->returnValue($this->expectPromiseOnce()));
28
29 $retryExecutor = new RetryExecutor($executor, 2);
30
31 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
32 $retryExecutor->query('8.8.8.8', $query);
33 }
34
35 /**
36 * @covers React\Dns\Query\RetryExecutor
37 * @test
38 */
39 public function queryShouldRetryQueryOnTimeout()
40 {
41 $response = $this->createStandardResponse();
42
43 $executor = $this->createExecutorMock();
44 $executor
45 ->expects($this->exactly(2))
46 ->method('query')
47 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
48 ->will($this->onConsecutiveCalls(
49 $this->returnCallback(function ($domain, $query) {
50 return Promise\reject(new TimeoutException("timeout"));
51 }),
52 $this->returnCallback(function ($domain, $query) use ($response) {
53 return Promise\resolve($response);
54 })
55 ));
56
57 $callback = $this->createCallableMock();
58 $callback
59 ->expects($this->once())
60 ->method('__invoke')
61 ->with($this->isInstanceOf('React\Dns\Model\Message'));
62
63 $errorback = $this->expectCallableNever();
64
65 $retryExecutor = new RetryExecutor($executor, 2);
66
67 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
68 $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback);
69 }
70
71 /**
72 * @covers React\Dns\Query\RetryExecutor
73 * @test
74 */
75 public function queryShouldStopRetryingAfterSomeAttempts()
76 {
77 $executor = $this->createExecutorMock();
78 $executor
79 ->expects($this->exactly(3))
80 ->method('query')
81 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
82 ->will($this->returnCallback(function ($domain, $query) {
83 return Promise\reject(new TimeoutException("timeout"));
84 }));
85
86 $callback = $this->expectCallableNever();
87
88 $errorback = $this->createCallableMock();
89 $errorback
90 ->expects($this->once())
91 ->method('__invoke')
92 ->with($this->isInstanceOf('RuntimeException'));
93
94 $retryExecutor = new RetryExecutor($executor, 2);
95
96 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
97 $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback);
98 }
99
100 /**
101 * @covers React\Dns\Query\RetryExecutor
102 * @test
103 */
104 public function queryShouldForwardNonTimeoutErrors()
105 {
106 $executor = $this->createExecutorMock();
107 $executor
108 ->expects($this->once())
109 ->method('query')
110 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
111 ->will($this->returnCallback(function ($domain, $query) {
112 return Promise\reject(new \Exception);
113 }));
114
115 $callback = $this->expectCallableNever();
116
117 $errorback = $this->createCallableMock();
118 $errorback
119 ->expects($this->once())
120 ->method('__invoke')
121 ->with($this->isInstanceOf('Exception'));
122
123 $retryExecutor = new RetryExecutor($executor, 2);
124
125 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
126 $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback);
127 }
128
129 /**
130 * @covers React\Dns\Query\RetryExecutor
131 * @test
132 */
133 public function queryShouldCancelQueryOnCancel()
134 {
135 $cancelled = 0;
136
137 $executor = $this->createExecutorMock();
138 $executor
139 ->expects($this->once())
140 ->method('query')
141 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
142 ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) {
143 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
144 ++$cancelled;
145 $reject(new CancellationException('Cancelled'));
146 });
147
148 return $deferred->promise();
149 })
150 );
151
152 $retryExecutor = new RetryExecutor($executor, 2);
153
154 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
155 $promise = $retryExecutor->query('8.8.8.8', $query);
156
157 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
158
159 $this->assertEquals(0, $cancelled);
160 $promise->cancel();
161 $this->assertEquals(1, $cancelled);
162 }
163
164 protected function expectPromiseOnce($return = null)
165 {
166 $mock = $this->createPromiseMock();
167 $mock
168 ->expects($this->once())
169 ->method('then')
170 ->will($this->returnValue($return));
171
172 return $mock;
173 }
174
175 protected function createExecutorMock()
176 {
177 return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
178 }
179
180 protected function createPromiseMock()
181 {
182 return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock();
183 }
184
185 protected function createStandardResponse()
186 {
187 $response = new Message();
188 $response->header->set('qr', 1);
189 $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN);
190 $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131');
191 $response->prepare();
192
193 return $response;
194 }
195 }
196
0 <?php
1
2 namespace React\Tests\Dns\Query;
3
4 use React\Dns\Query\TimeoutExecutor;
5 use React\Dns\Query\Query;
6 use React\Dns\Model\Message;
7 use React\Promise\Deferred;
8 use React\Dns\Query\CancellationException;
9 use React\Tests\Dns\TestCase;
10 use React\EventLoop\Factory;
11 use React\Promise;
12
13 class TimeoutExecutorTest extends TestCase
14 {
15 public function setUp()
16 {
17 $this->loop = Factory::create();
18
19 $this->wrapped = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
20
21 $this->executor = new TimeoutExecutor($this->wrapped, 5.0, $this->loop);
22 }
23
24 public function testCancellingPromiseWillCancelWrapped()
25 {
26 $cancelled = 0;
27
28 $this->wrapped
29 ->expects($this->once())
30 ->method('query')
31 ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) {
32 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
33 ++$cancelled;
34 $reject(new CancellationException('Cancelled'));
35 });
36
37 return $deferred->promise();
38 }));
39
40 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
41 $promise = $this->executor->query('8.8.8.8:53', $query);
42
43 $this->assertEquals(0, $cancelled);
44 $promise->cancel();
45 $this->assertEquals(1, $cancelled);
46
47 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
48 }
49
50 public function testResolvesPromiseWhenWrappedResolves()
51 {
52 $this->wrapped
53 ->expects($this->once())
54 ->method('query')
55 ->willReturn(Promise\resolve('0.0.0.0'));
56
57 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
58 $promise = $this->executor->query('8.8.8.8:53', $query);
59
60 $promise->then($this->expectCallableOnce(), $this->expectCallableNever());
61 }
62
63 public function testRejectsPromiseWhenWrappedRejects()
64 {
65 $this->wrapped
66 ->expects($this->once())
67 ->method('query')
68 ->willReturn(Promise\reject(new \RuntimeException()));
69
70 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
71 $promise = $this->executor->query('8.8.8.8:53', $query);
72
73 $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith(new \RuntimeException()));
74 }
75
76 public function testWrappedWillBeCancelledOnTimeout()
77 {
78 $this->executor = new TimeoutExecutor($this->wrapped, 0, $this->loop);
79
80 $cancelled = 0;
81
82 $this->wrapped
83 ->expects($this->once())
84 ->method('query')
85 ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) {
86 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
87 ++$cancelled;
88 $reject(new CancellationException('Cancelled'));
89 });
90
91 return $deferred->promise();
92 }));
93
94 $callback = $this->expectCallableNever();
95
96 $errorback = $this->createCallableMock();
97 $errorback
98 ->expects($this->once())
99 ->method('__invoke')
100 ->with($this->logicalAnd(
101 $this->isInstanceOf('React\Dns\Query\TimeoutException'),
102 $this->attribute($this->equalTo('DNS query for igor.io timed out'), 'message')
103 ));
104
105 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
106 $this->executor->query('8.8.8.8:53', $query)->then($callback, $errorback);
107
108 $this->assertEquals(0, $cancelled);
109
110 $this->loop->run();
111
112 $this->assertEquals(1, $cancelled);
113 }
114 }
0 <?php
1
2 namespace React\Tests\Dns\Resolver;
3
4 use React\Dns\Resolver\Factory;
5 use React\Tests\Dns\TestCase;
6 use React\Dns\Query\HostsFileExecutor;
7
8 class FactoryTest extends TestCase
9 {
10 /** @test */
11 public function createShouldCreateResolver()
12 {
13 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
14
15 $factory = new Factory();
16 $resolver = $factory->create('8.8.8.8:53', $loop);
17
18 $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
19 }
20
21 /** @test */
22 public function createWithoutPortShouldCreateResolverWithDefaultPort()
23 {
24 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
25
26 $factory = new Factory();
27 $resolver = $factory->create('8.8.8.8', $loop);
28
29 $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
30 $this->assertSame('8.8.8.8:53', $this->getResolverPrivateMemberValue($resolver, 'nameserver'));
31 }
32
33 /** @test */
34 public function createCachedShouldCreateResolverWithCachedExecutor()
35 {
36 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
37
38 $factory = new Factory();
39 $resolver = $factory->createCached('8.8.8.8:53', $loop);
40
41 $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
42 $executor = $this->getResolverPrivateExecutor($resolver);
43 $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor);
44 $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache');
45 $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache');
46 $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache);
47 $this->assertInstanceOf('React\Cache\ArrayCache', $recordCacheCache);
48 }
49
50 /** @test */
51 public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCache()
52 {
53 $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock();
54 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
55
56 $factory = new Factory();
57 $resolver = $factory->createCached('8.8.8.8:53', $loop, $cache);
58
59 $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
60 $executor = $this->getResolverPrivateExecutor($resolver);
61 $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor);
62 $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache');
63 $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache');
64 $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache);
65 $this->assertSame($cache, $recordCacheCache);
66 }
67
68 /**
69 * @test
70 * @dataProvider factoryShouldAddDefaultPortProvider
71 */
72 public function factoryShouldAddDefaultPort($input, $expected)
73 {
74 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
75
76 $factory = new Factory();
77 $resolver = $factory->create($input, $loop);
78
79 $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
80 $this->assertSame($expected, $this->getResolverPrivateMemberValue($resolver, 'nameserver'));
81 }
82
83 public static function factoryShouldAddDefaultPortProvider()
84 {
85 return array(
86 array('8.8.8.8', '8.8.8.8:53'),
87 array('1.2.3.4:5', '1.2.3.4:5'),
88 array('localhost', 'localhost:53'),
89 array('localhost:1234', 'localhost:1234'),
90 array('::1', '[::1]:53'),
91 array('[::1]:53', '[::1]:53')
92 );
93 }
94
95 private function getResolverPrivateExecutor($resolver)
96 {
97 $executor = $this->getResolverPrivateMemberValue($resolver, 'executor');
98
99 // extract underlying executor that may be wrapped in multiple layers of hosts file executors
100 while ($executor instanceof HostsFileExecutor) {
101 $reflector = new \ReflectionProperty('React\Dns\Query\HostsFileExecutor', 'fallback');
102 $reflector->setAccessible(true);
103
104 $executor = $reflector->getValue($executor);
105 }
106
107 return $executor;
108 }
109
110 private function getResolverPrivateMemberValue($resolver, $field)
111 {
112 $reflector = new \ReflectionProperty('React\Dns\Resolver\Resolver', $field);
113 $reflector->setAccessible(true);
114 return $reflector->getValue($resolver);
115 }
116
117 private function getCachedExecutorPrivateMemberValue($resolver, $field)
118 {
119 $reflector = new \ReflectionProperty('React\Dns\Query\CachedExecutor', $field);
120 $reflector->setAccessible(true);
121 return $reflector->getValue($resolver);
122 }
123
124 private function getRecordCachePrivateMemberValue($resolver, $field)
125 {
126 $reflector = new \ReflectionProperty('React\Dns\Query\RecordCache', $field);
127 $reflector->setAccessible(true);
128 return $reflector->getValue($resolver);
129 }
130 }
0 <?php
1
2 namespace React\Tests\Dns\Resolver;
3
4 use PHPUnit\Framework\TestCase;
5 use React\Dns\Resolver\Resolver;
6 use React\Dns\Query\Query;
7 use React\Dns\Model\Message;
8 use React\Dns\Model\Record;
9
10 class ResolveAliasesTest extends TestCase
11 {
12 /**
13 * @covers React\Dns\Resolver\Resolver::resolveAliases
14 * @dataProvider provideAliasedAnswers
15 */
16 public function testResolveAliases(array $expectedAnswers, array $answers, $name)
17 {
18 $executor = $this->createExecutorMock();
19 $resolver = new Resolver('8.8.8.8:53', $executor);
20
21 $answers = $resolver->resolveAliases($answers, $name);
22
23 $this->assertEquals($expectedAnswers, $answers);
24 }
25
26 public function provideAliasedAnswers()
27 {
28 return array(
29 array(
30 array('178.79.169.131'),
31 array(
32 new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
33 ),
34 'igor.io',
35 ),
36 array(
37 array('178.79.169.131', '178.79.169.132', '178.79.169.133'),
38 array(
39 new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
40 new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'),
41 new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.133'),
42 ),
43 'igor.io',
44 ),
45 array(
46 array('178.79.169.131'),
47 array(
48 new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
49 new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
50 new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
51 ),
52 'igor.io',
53 ),
54 array(
55 array(),
56 array(
57 new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN),
58 new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN),
59 ),
60 'igor.io',
61 ),
62 array(
63 array('178.79.169.131'),
64 array(
65 new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'),
66 new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
67 ),
68 'igor.io',
69 ),
70 array(
71 array('178.79.169.131'),
72 array(
73 new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'),
74 new Record('foo.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'bar.igor.io'),
75 new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
76 ),
77 'igor.io',
78 ),
79 array(
80 array('178.79.169.131', '178.79.169.132', '178.79.169.133'),
81 array(
82 new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'),
83 new Record('foo.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'bar.igor.io'),
84 new Record('bar.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'baz.igor.io'),
85 new Record('bar.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'qux.igor.io'),
86 new Record('baz.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
87 new Record('baz.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'),
88 new Record('qux.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.133'),
89 ),
90 'igor.io',
91 ),
92 );
93 }
94
95 private function createExecutorMock()
96 {
97 return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
98 }
99 }
0 <?php
1
2 namespace React\Tests\Dns\Resolver;
3
4 use React\Dns\Resolver\Resolver;
5 use React\Dns\Query\Query;
6 use React\Dns\Model\Message;
7 use React\Dns\Model\Record;
8 use React\Promise;
9 use React\Tests\Dns\TestCase;
10
11 class ResolverTest extends TestCase
12 {
13 /** @test */
14 public function resolveShouldQueryARecords()
15 {
16 $executor = $this->createExecutorMock();
17 $executor
18 ->expects($this->once())
19 ->method('query')
20 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
21 ->will($this->returnCallback(function ($nameserver, $query) {
22 $response = new Message();
23 $response->header->set('qr', 1);
24 $response->questions[] = new Record($query->name, $query->type, $query->class);
25 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '178.79.169.131');
26
27 return Promise\resolve($response);
28 }));
29
30 $resolver = new Resolver('8.8.8.8:53', $executor);
31 $resolver->resolve('igor.io')->then($this->expectCallableOnceWith('178.79.169.131'));
32 }
33
34 /** @test */
35 public function resolveShouldQueryARecordsAndIgnoreCase()
36 {
37 $executor = $this->createExecutorMock();
38 $executor
39 ->expects($this->once())
40 ->method('query')
41 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
42 ->will($this->returnCallback(function ($nameserver, $query) {
43 $response = new Message();
44 $response->header->set('qr', 1);
45 $response->questions[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class);
46 $response->answers[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class, 3600, '178.79.169.131');
47
48 return Promise\resolve($response);
49 }));
50
51 $resolver = new Resolver('8.8.8.8:53', $executor);
52 $resolver->resolve('blog.wyrihaximus.net')->then($this->expectCallableOnceWith('178.79.169.131'));
53 }
54
55 /** @test */
56 public function resolveShouldFilterByName()
57 {
58 $executor = $this->createExecutorMock();
59 $executor
60 ->expects($this->once())
61 ->method('query')
62 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
63 ->will($this->returnCallback(function ($nameserver, $query) {
64 $response = new Message();
65 $response->header->set('qr', 1);
66 $response->questions[] = new Record($query->name, $query->type, $query->class);
67 $response->answers[] = new Record('foo.bar', $query->type, $query->class, 3600, '178.79.169.131');
68
69 return Promise\resolve($response);
70 }));
71
72 $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException'));
73
74 $resolver = new Resolver('8.8.8.8:53', $executor);
75 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
76 }
77
78 /** @test */
79 public function resolveWithNoAnswersShouldThrowException()
80 {
81 $executor = $this->createExecutorMock();
82 $executor
83 ->expects($this->once())
84 ->method('query')
85 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
86 ->will($this->returnCallback(function ($nameserver, $query) {
87 $response = new Message();
88 $response->header->set('qr', 1);
89 $response->questions[] = new Record($query->name, $query->type, $query->class);
90
91 return Promise\resolve($response);
92 }));
93
94 $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException'));
95
96 $resolver = new Resolver('8.8.8.8:53', $executor);
97 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
98 }
99
100 /**
101 * @test
102 */
103 public function resolveWithNoAnswersShouldCallErrbackIfGiven()
104 {
105 $executor = $this->createExecutorMock();
106 $executor
107 ->expects($this->once())
108 ->method('query')
109 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
110 ->will($this->returnCallback(function ($nameserver, $query) {
111 $response = new Message();
112 $response->header->set('qr', 1);
113 $response->questions[] = new Record($query->name, $query->type, $query->class);
114
115 return Promise\resolve($response);
116 }));
117
118 $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException'));
119
120 $resolver = new Resolver('8.8.8.8:53', $executor);
121 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
122 }
123
124 private function createExecutorMock()
125 {
126 return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();
127 }
128 }
0 <?php
1
2 namespace React\Tests\Dns;
3
4 use PHPUnit\Framework\TestCase as BaseTestCase;
5
6 abstract class TestCase extends BaseTestCase
7 {
8 protected function expectCallableOnce()
9 {
10 $mock = $this->createCallableMock();
11 $mock
12 ->expects($this->once())
13 ->method('__invoke');
14
15 return $mock;
16 }
17
18 protected function expectCallableOnceWith($value)
19 {
20 $mock = $this->createCallableMock();
21 $mock
22 ->expects($this->once())
23 ->method('__invoke')
24 ->with($value);
25
26 return $mock;
27 }
28
29 protected function expectCallableNever()
30 {
31 $mock = $this->createCallableMock();
32 $mock
33 ->expects($this->never())
34 ->method('__invoke');
35
36 return $mock;
37 }
38
39 protected function createCallableMock()
40 {
41 return $this->getMockBuilder('React\Tests\Dns\CallableStub')->getMock();
42 }
43
44 public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null)
45 {
46 if (method_exists($this, 'expectException')) {
47 // PHPUnit 5
48 $this->expectException($exception);
49 if ($exceptionMessage !== '') {
50 $this->expectExceptionMessage($exceptionMessage);
51 }
52 if ($exceptionCode !== null) {
53 $this->expectExceptionCode($exceptionCode);
54 }
55 } else {
56 // legacy PHPUnit 4
57 parent::setExpectedException($exception, $exceptionMessage, $exceptionCode);
58 }
59 }
60 }