New upstream version 0.4.15
mirabilos authored 5 years ago
mirabilos committed 5 years ago
0 | 0 | # Changelog |
1 | ||
2 | ## 0.4.15 (2018-07-02) | |
3 | ||
4 | * Feature: Add `resolveAll()` method to support custom query types in `Resolver`. | |
5 | (#110 by @clue and @WyriHaximus) | |
6 | ||
7 | ```php | |
8 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { | |
9 | echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
10 | }); | |
11 | ``` | |
12 | ||
13 | * Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records. | |
14 | (#104, #105, #106, #107 and #108 by @clue) | |
15 | ||
16 | * Feature: Add support for `Message::TYPE_ANY` parse unknown types as binary data. | |
17 | (#104 by @clue) | |
18 | ||
19 | * Feature: Improve error messages for failed queries and improve documentation. | |
20 | (#109 by @clue) | |
21 | ||
22 | * Feature: Add reverse DNS lookup example. | |
23 | (#111 by @clue) | |
24 | ||
25 | ## 0.4.14 (2018-06-26) | |
26 | ||
27 | * Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages | |
28 | to avoid cache poisoning attacks and deprecate legacy `Executor`. | |
29 | (#101 and #103 by @clue) | |
30 | ||
31 | * Feature: Forward compatibility with Cache 0.5 | |
32 | (#102 by @clue) | |
33 | ||
34 | * Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API. | |
35 | (#99 by @clue) | |
1 | 36 | |
2 | 37 | ## 0.4.13 (2018-02-27) |
3 | 38 |
12 | 12 | * [Basic usage](#basic-usage) |
13 | 13 | * [Caching](#caching) |
14 | 14 | * [Custom cache adapter](#custom-cache-adapter) |
15 | * [Resolver](#resolver) | |
16 | * [resolve()](#resolve) | |
17 | * [resolveAll()](#resolveall) | |
15 | 18 | * [Advanced usage](#advanced-usage) |
19 | * [UdpTransportExecutor](#udptransportexecutor) | |
16 | 20 | * [HostsFileExecutor](#hostsfileexecutor) |
17 | 21 | * [Install](#install) |
18 | 22 | * [Tests](#tests) |
56 | 60 | Ideally, this method should thus be executed only once before the loop starts |
57 | 61 | and not repeatedly while it is running. |
58 | 62 | |
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 | 63 | But there's more. |
68 | 64 | |
69 | 65 | ## Caching |
114 | 110 | |
115 | 111 | See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). |
116 | 112 | |
113 | ## Resolver | |
114 | ||
115 | ### resolve() | |
116 | ||
117 | The `resolve(string $domain): PromiseInterface<string,Exception>` method can be used to | |
118 | resolve the given $domain name to a single IPv4 address (type `A` query). | |
119 | ||
120 | ```php | |
121 | $resolver->resolve('reactphp.org')->then(function ($ip) { | |
122 | echo 'IP for reactphp.org is ' . $ip . PHP_EOL; | |
123 | }); | |
124 | ``` | |
125 | ||
126 | This is one of the main methods in this package. It sends a DNS query | |
127 | for the given $domain name to your DNS server and returns a single IP | |
128 | address on success. | |
129 | ||
130 | If the DNS server sends a DNS response message that contains more than | |
131 | one IP address for this query, it will randomly pick one of the IP | |
132 | addresses from the response. If you want the full list of IP addresses | |
133 | or want to send a different type of query, you should use the | |
134 | [`resolveAll()`](#resolveall) method instead. | |
135 | ||
136 | If the DNS server sends a DNS response message that indicates an error | |
137 | code, this method will reject with a `RecordNotFoundException`. Its | |
138 | message and code can be used to check for the response code. | |
139 | ||
140 | If the DNS communication fails and the server does not respond with a | |
141 | valid response message, this message will reject with an `Exception`. | |
142 | ||
143 | Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
144 | ||
145 | ```php | |
146 | $promise = $resolver->resolve('reactphp.org'); | |
147 | ||
148 | $promise->cancel(); | |
149 | ``` | |
150 | ||
151 | ### resolveAll() | |
152 | ||
153 | The `resolveAll(string $host, int $type): PromiseInterface<array,Exception>` method can be used to | |
154 | resolve all record values for the given $domain name and query $type. | |
155 | ||
156 | ```php | |
157 | $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { | |
158 | echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
159 | }); | |
160 | ||
161 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { | |
162 | echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
163 | }); | |
164 | ``` | |
165 | ||
166 | This is one of the main methods in this package. It sends a DNS query | |
167 | for the given $domain name to your DNS server and returns a list with all | |
168 | record values on success. | |
169 | ||
170 | If the DNS server sends a DNS response message that contains one or more | |
171 | records for this query, it will return a list with all record values | |
172 | from the response. You can use the `Message::TYPE_*` constants to control | |
173 | which type of query will be sent. Note that this method always returns a | |
174 | list of record values, but each record value type depends on the query | |
175 | type. For example, it returns the IPv4 addresses for type `A` queries, | |
176 | the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, | |
177 | `CNAME` and `PTR` queries and structured data for other queries. See also | |
178 | the `Record` documentation for more details. | |
179 | ||
180 | If the DNS server sends a DNS response message that indicates an error | |
181 | code, this method will reject with a `RecordNotFoundException`. Its | |
182 | message and code can be used to check for the response code. | |
183 | ||
184 | If the DNS communication fails and the server does not respond with a | |
185 | valid response message, this message will reject with an `Exception`. | |
186 | ||
187 | Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
188 | ||
189 | ```php | |
190 | $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); | |
191 | ||
192 | $promise->cancel(); | |
193 | ``` | |
194 | ||
117 | 195 | ## Advanced Usage |
118 | 196 | |
119 | For more advanced usages one can utilize the `React\Dns\Query\Executor` directly. | |
197 | ### UdpTransportExecutor | |
198 | ||
199 | The `UdpTransportExecutor` can be used to | |
200 | send DNS queries over a UDP transport. | |
201 | ||
202 | This is the main class that sends a DNS query to your DNS server and is used | |
203 | internally by the `Resolver` for the actual message transport. | |
204 | ||
205 | For more advanced usages one can utilize this class directly. | |
120 | 206 | The following example looks up the `IPv6` address for `igor.io`. |
121 | 207 | |
122 | 208 | ```php |
123 | 209 | $loop = Factory::create(); |
124 | ||
125 | $executor = new Executor($loop, new Parser(), new BinaryDumper(), null); | |
210 | $executor = new UdpTransportExecutor($loop); | |
126 | 211 | |
127 | 212 | $executor->query( |
128 | 213 | '8.8.8.8:53', |
129 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time()) | |
130 | )->done(function (Message $message) { | |
214 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
215 | )->then(function (Message $message) { | |
131 | 216 | foreach ($message->answers as $answer) { |
132 | 217 | echo 'IPv6: ' . $answer->data . PHP_EOL; |
133 | 218 | } |
134 | 219 | }, 'printf'); |
135 | 220 | |
136 | 221 | $loop->run(); |
137 | ||
138 | 222 | ``` |
139 | 223 | |
140 | 224 | See also the [fourth example](examples). |
141 | 225 | |
226 | Note that this executor does not implement a timeout, so you will very likely | |
227 | want to use this in combination with a `TimeoutExecutor` like this: | |
228 | ||
229 | ```php | |
230 | $executor = new TimeoutExecutor( | |
231 | new UdpTransportExecutor($loop), | |
232 | 3.0, | |
233 | $loop | |
234 | ); | |
235 | ``` | |
236 | ||
237 | Also note that this executor uses an unreliable UDP transport and that it | |
238 | does not implement any retry logic, so you will likely want to use this in | |
239 | combination with a `RetryExecutor` like this: | |
240 | ||
241 | ```php | |
242 | $executor = new RetryExecutor( | |
243 | new TimeoutExecutor( | |
244 | new UdpTransportExecutor($loop), | |
245 | 3.0, | |
246 | $loop | |
247 | ) | |
248 | ); | |
249 | ``` | |
250 | ||
251 | > Internally, this class uses PHP's UDP sockets and does not take advantage | |
252 | of [react/datagram](https://github.com/reactphp/datagram) purely for | |
253 | organizational reasons to avoid a cyclic dependency between the two | |
254 | packages. Higher-level components should take advantage of the Datagram | |
255 | component instead of reimplementing this socket logic from scratch. | |
256 | ||
142 | 257 | ### HostsFileExecutor |
143 | 258 | |
144 | Note that the above `Executor` class always performs an actual DNS query. | |
259 | Note that the above `UdpTransportExecutor` class always performs an actual DNS query. | |
145 | 260 | If you also want to take entries from your hosts file into account, you may |
146 | 261 | use this code: |
147 | 262 | |
148 | 263 | ```php |
149 | 264 | $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); |
150 | 265 | |
151 | $executor = new Executor($loop, new Parser(), new BinaryDumper(), null); | |
266 | $executor = new UdpTransportExecutor($loop); | |
152 | 267 | $executor = new HostsFileExecutor($hosts, $executor); |
153 | 268 | |
154 | 269 | $executor->query( |
155 | 270 | '8.8.8.8:53', |
156 | new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time()) | |
271 | new Query('localhost', Message::TYPE_A, Message::CLASS_IN) | |
157 | 272 | ); |
158 | 273 | ``` |
159 | 274 | |
165 | 280 | This will install the latest supported version: |
166 | 281 | |
167 | 282 | ```bash |
168 | $ composer require react/dns:^0.4.13 | |
283 | $ composer require react/dns:^0.4.15 | |
169 | 284 | ``` |
170 | 285 | |
171 | 286 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. |
4 | 4 | "license": "MIT", |
5 | 5 | "require": { |
6 | 6 | "php": ">=5.3.0", |
7 | "react/cache": "~0.4.0|~0.3.0", | |
7 | "react/cache": "^0.5 || ^0.4 || ^0.3", | |
8 | 8 | "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", |
9 | 9 | "react/promise": "^2.1 || ^1.2.1", |
10 | 10 | "react/promise-timer": "^1.2", |
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 | <?php | |
1 | ||
2 | use React\Dns\Config\Config; | |
3 | use React\Dns\Resolver\Factory; | |
4 | use React\Dns\Model\Message; | |
5 | ||
6 | require __DIR__ . '/../vendor/autoload.php'; | |
7 | ||
8 | $loop = React\EventLoop\Factory::create(); | |
9 | ||
10 | $config = Config::loadSystemConfigBlocking(); | |
11 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
12 | ||
13 | $factory = new Factory(); | |
14 | $resolver = $factory->create($server, $loop); | |
15 | ||
16 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; | |
17 | ||
18 | $resolver->resolveAll($name, Message::TYPE_A)->then(function (array $ips) use ($name) { | |
19 | echo 'IPv4 addresses for ' . $name . ': ' . implode(', ', $ips) . PHP_EOL; | |
20 | }, function (Exception $e) use ($name) { | |
21 | echo 'No IPv4 addresses for ' . $name . ': ' . $e->getMessage() . PHP_EOL; | |
22 | }); | |
23 | ||
24 | $resolver->resolveAll($name, Message::TYPE_AAAA)->then(function (array $ips) use ($name) { | |
25 | echo 'IPv6 addresses for ' . $name . ': ' . implode(', ', $ips) . PHP_EOL; | |
26 | }, function (Exception $e) use ($name) { | |
27 | echo 'No IPv6 addresses for ' . $name . ': ' . $e->getMessage() . PHP_EOL; | |
28 | }); | |
29 | ||
30 | $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 | $name = isset($argv[1]) ? $argv[1] : 'google.com'; | |
16 | $type = constant('React\Dns\Model\Message::TYPE_' . (isset($argv[2]) ? $argv[2] : 'TXT')); | |
17 | ||
18 | $resolver->resolveAll($name, $type)->then(function (array $values) { | |
19 | var_dump($values); | |
20 | }, function (Exception $e) { | |
21 | echo $e->getMessage() . PHP_EOL; | |
22 | }); | |
23 | ||
24 | $loop->run(); |
0 | <?php | |
1 | ||
2 | use React\Dns\Config\Config; | |
3 | use React\Dns\Resolver\Factory; | |
4 | use React\Dns\Model\Message; | |
5 | ||
6 | require __DIR__ . '/../vendor/autoload.php'; | |
7 | ||
8 | $loop = React\EventLoop\Factory::create(); | |
9 | ||
10 | $config = Config::loadSystemConfigBlocking(); | |
11 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
12 | ||
13 | $factory = new Factory(); | |
14 | $resolver = $factory->create($server, $loop); | |
15 | ||
16 | $ip = isset($argv[1]) ? $argv[1] : '8.8.8.8'; | |
17 | ||
18 | if (@inet_pton($ip) === false) { | |
19 | exit('Error: Given argument is not a valid IP' . PHP_EOL); | |
20 | } | |
21 | ||
22 | if (strpos($ip, ':') === false) { | |
23 | $name = inet_ntop(strrev(inet_pton($ip))) . '.in-addr.arpa'; | |
24 | } else { | |
25 | $name = wordwrap(strrev(bin2hex(inet_pton($ip))), 1, '.', true) . '.ip6.arpa'; | |
26 | } | |
27 | ||
28 | $resolver->resolveAll($name, Message::TYPE_PTR)->then(function (array $names) { | |
29 | var_dump($names); | |
30 | }, function (Exception $e) { | |
31 | echo $e->getMessage() . PHP_EOL; | |
32 | }); | |
33 | ||
34 | $loop->run(); |
0 | <?php | |
1 | ||
2 | use React\Dns\Model\Message; | |
3 | use React\Dns\Query\Query; | |
4 | use React\Dns\Query\UdpTransportExecutor; | |
5 | use React\EventLoop\Factory; | |
6 | ||
7 | require __DIR__ . '/../vendor/autoload.php'; | |
8 | ||
9 | $loop = Factory::create(); | |
10 | $executor = new UdpTransportExecutor($loop); | |
11 | ||
12 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; | |
13 | ||
14 | $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN); | |
15 | $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN); | |
16 | ||
17 | $executor->query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) { | |
18 | foreach ($message->answers as $answer) { | |
19 | echo 'IPv4: ' . $answer->data . PHP_EOL; | |
20 | } | |
21 | }, 'printf'); | |
22 | $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) { | |
23 | foreach ($message->answers as $answer) { | |
24 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
25 | } | |
26 | }, 'printf'); | |
27 | ||
28 | $loop->run(); |
0 | <?php | |
1 | ||
2 | use React\Dns\Model\Message; | |
3 | use React\Dns\Model\Record; | |
4 | use React\Dns\Query\Query; | |
5 | use React\Dns\Query\UdpTransportExecutor; | |
6 | use React\EventLoop\Factory; | |
7 | ||
8 | require __DIR__ . '/../vendor/autoload.php'; | |
9 | ||
10 | $loop = Factory::create(); | |
11 | $executor = new UdpTransportExecutor($loop); | |
12 | ||
13 | $name = isset($argv[1]) ? $argv[1] : 'google.com'; | |
14 | ||
15 | $any = new Query($name, Message::TYPE_ANY, Message::CLASS_IN); | |
16 | ||
17 | $executor->query('8.8.8.8:53', $any)->then(function (Message $message) { | |
18 | foreach ($message->answers as $answer) { | |
19 | /* @var $answer Record */ | |
20 | ||
21 | $data = $answer->data; | |
22 | ||
23 | switch ($answer->type) { | |
24 | case Message::TYPE_A: | |
25 | $type = 'A'; | |
26 | break; | |
27 | case Message::TYPE_AAAA: | |
28 | $type = 'AAAA'; | |
29 | break; | |
30 | case Message::TYPE_NS: | |
31 | $type = 'NS'; | |
32 | break; | |
33 | case Message::TYPE_PTR: | |
34 | $type = 'PTR'; | |
35 | break; | |
36 | case Message::TYPE_CNAME: | |
37 | $type = 'CNAME'; | |
38 | break; | |
39 | case Message::TYPE_TXT: | |
40 | // TXT records can contain a list of (binary) strings for each record. | |
41 | // here, we assume this is printable ASCII and simply concatenate output | |
42 | $type = 'TXT'; | |
43 | $data = implode('', $data); | |
44 | break; | |
45 | case Message::TYPE_MX: | |
46 | // MX records contain "priority" and "target", only dump its values here | |
47 | $type = 'MX'; | |
48 | $data = implode(' ', $data); | |
49 | break; | |
50 | case Message::TYPE_SRV: | |
51 | // SRV records contains priority, weight, port and target, dump structure here | |
52 | $type = 'SRV'; | |
53 | $data = json_encode($data); | |
54 | break; | |
55 | case Message::TYPE_SOA: | |
56 | // SOA records contain structured data, dump structure here | |
57 | $type = 'SOA'; | |
58 | $data = json_encode($data); | |
59 | break; | |
60 | default: | |
61 | // unknown type uses HEX format | |
62 | $type = 'Type ' . $answer->type; | |
63 | $data = wordwrap(strtoupper(bin2hex($data)), 2, ' ', true); | |
64 | } | |
65 | ||
66 | echo $type . ': ' . $data . PHP_EOL; | |
67 | } | |
68 | }, 'printf'); | |
69 | ||
70 | $loop->run(); |
3 | 3 | |
4 | 4 | class HeaderBag |
5 | 5 | { |
6 | public $data = ''; | |
7 | ||
8 | 6 | public $attributes = array( |
9 | 7 | 'qdCount' => 0, |
10 | 8 | 'anCount' => 0, |
19 | 17 | 'z' => 0, |
20 | 18 | 'rcode' => Message::RCODE_OK, |
21 | 19 | ); |
20 | ||
21 | /** | |
22 | * @deprecated unused, exists for BC only | |
23 | */ | |
24 | public $data = ''; | |
22 | 25 | |
23 | 26 | public function get($name) |
24 | 27 | { |
2 | 2 | namespace React\Dns\Model; |
3 | 3 | |
4 | 4 | use React\Dns\Query\Query; |
5 | use React\Dns\Model\Record; | |
6 | 5 | |
7 | 6 | class Message |
8 | 7 | { |
14 | 13 | const TYPE_MX = 15; |
15 | 14 | const TYPE_TXT = 16; |
16 | 15 | const TYPE_AAAA = 28; |
16 | const TYPE_SRV = 33; | |
17 | const TYPE_ANY = 255; | |
17 | 18 | |
18 | 19 | const CLASS_IN = 1; |
19 | 20 | |
72 | 73 | return $response; |
73 | 74 | } |
74 | 75 | |
76 | /** | |
77 | * generates a random 16 bit message ID | |
78 | * | |
79 | * This uses a CSPRNG so that an outside attacker that is sending spoofed | |
80 | * DNS response messages can not guess the message ID to avoid possible | |
81 | * cache poisoning attacks. | |
82 | * | |
83 | * The `random_int()` function is only available on PHP 7+ or when | |
84 | * https://github.com/paragonie/random_compat is installed. As such, using | |
85 | * the latest supported PHP version is highly recommended. This currently | |
86 | * falls back to a less secure random number generator on older PHP versions | |
87 | * in the hope that this system is properly protected against outside | |
88 | * attackers, for example by using one of the common local DNS proxy stubs. | |
89 | * | |
90 | * @return int | |
91 | * @see self::getId() | |
92 | * @codeCoverageIgnore | |
93 | */ | |
75 | 94 | private static function generateId() |
76 | 95 | { |
96 | if (function_exists('random_int')) { | |
97 | return random_int(0, 0xffff); | |
98 | } | |
77 | 99 | return mt_rand(0, 0xffff); |
78 | 100 | } |
79 | ||
80 | public $data = ''; | |
81 | 101 | |
82 | 102 | public $header; |
83 | 103 | public $questions = array(); |
85 | 105 | public $authority = array(); |
86 | 106 | public $additional = array(); |
87 | 107 | |
108 | /** | |
109 | * @deprecated still used internally for BC reasons, should not be used externally. | |
110 | */ | |
111 | public $data = ''; | |
112 | ||
113 | /** | |
114 | * @deprecated still used internally for BC reasons, should not be used externally. | |
115 | */ | |
88 | 116 | public $consumed = 0; |
89 | 117 | |
90 | 118 | public function __construct() |
92 | 120 | $this->header = new HeaderBag(); |
93 | 121 | } |
94 | 122 | |
123 | /** | |
124 | * Returns the 16 bit message ID | |
125 | * | |
126 | * The response message ID has to match the request message ID. This allows | |
127 | * the receiver to verify this is the correct response message. An outside | |
128 | * attacker may try to inject fake responses by "guessing" the message ID, | |
129 | * so this should use a proper CSPRNG to avoid possible cache poisoning. | |
130 | * | |
131 | * @return int | |
132 | * @see self::generateId() | |
133 | */ | |
134 | public function getId() | |
135 | { | |
136 | return $this->header->get('id'); | |
137 | } | |
138 | ||
139 | /** | |
140 | * Returns the response code (RCODE) | |
141 | * | |
142 | * @return int see self::RCODE_* constants | |
143 | */ | |
144 | public function getResponseCode() | |
145 | { | |
146 | return $this->header->get('rcode'); | |
147 | } | |
148 | ||
95 | 149 | public function prepare() |
96 | 150 | { |
97 | 151 | $this->header->populateCounts($this); |
3 | 3 | |
4 | 4 | class Record |
5 | 5 | { |
6 | /** | |
7 | * @var string hostname without trailing dot, for example "reactphp.org" | |
8 | */ | |
6 | 9 | public $name; |
10 | ||
11 | /** | |
12 | * @var int see Message::TYPE_* constants (UINT16) | |
13 | */ | |
7 | 14 | public $type; |
15 | ||
16 | /** | |
17 | * @var int see Message::CLASS_IN constant (UINT16) | |
18 | */ | |
8 | 19 | public $class; |
20 | ||
21 | /** | |
22 | * @var int maximum TTL in seconds (UINT16) | |
23 | */ | |
9 | 24 | public $ttl; |
25 | ||
26 | /** | |
27 | * The payload data for this record | |
28 | * | |
29 | * The payload data format depends on the record type. As a rule of thumb, | |
30 | * this library will try to express this in a way that can be consumed | |
31 | * easily without having to worry about DNS internals and its binary transport: | |
32 | * | |
33 | * - A: | |
34 | * IPv4 address string, for example "192.168.1.1". | |
35 | * - AAAA: | |
36 | * IPv6 address string, for example "::1". | |
37 | * - CNAME / PTR / NS: | |
38 | * The hostname without trailing dot, for example "reactphp.org". | |
39 | * - TXT: | |
40 | * List of string values, for example `["v=spf1 include:example.com"]`. | |
41 | * This is commonly a list with only a single string value, but this | |
42 | * technically allows multiple strings (0-255 bytes each) in a single | |
43 | * record. This is rarely used and depending on application you may want | |
44 | * to join these together or handle them separately. Each string can | |
45 | * transport any binary data, its character encoding is not defined (often | |
46 | * ASCII/UTF-8 in practice). [RFC 1464](https://tools.ietf.org/html/rfc1464) | |
47 | * suggests using key-value pairs such as `["name=test","version=1"]`, but | |
48 | * interpretation of this is not enforced and left up to consumers of this | |
49 | * library (used for DNS-SD/Zeroconf and others). | |
50 | * - MX: | |
51 | * Mail server priority (UINT16) and target hostname without trailing dot, | |
52 | * for example `{"priority":10,"target":"mx.example.com"}`. | |
53 | * The payload data uses an associative array with fixed keys "priority" | |
54 | * (also commonly referred to as weight or preference) and "target" (also | |
55 | * referred to as exchange). If a response message contains multiple | |
56 | * records of this type, targets should be sorted by priority (lowest | |
57 | * first) - this is left up to consumers of this library (used for SMTP). | |
58 | * - SRV: | |
59 | * Service priority (UINT16), service weight (UINT16), service port (UINT16) | |
60 | * and target hostname without trailing dot, for example | |
61 | * `{"priority":10,"weight":50,"port":8080,"target":"example.com"}`. | |
62 | * The payload data uses an associative array with fixed keys "priority", | |
63 | * "weight", "port" and "target" (also referred to as name). | |
64 | * The target may be an empty host name string if the service is decidedly | |
65 | * not available. If a response message contains multiple records of this | |
66 | * type, targets should be sorted by priority (lowest first) and selected | |
67 | * randomly according to their weight - this is left up to consumers of | |
68 | * this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782) | |
69 | * for more details. | |
70 | * - SOA: | |
71 | * Includes master hostname without trailing dot, responsible person email | |
72 | * as hostname without trailing dot and serial, refresh, retry, expire and | |
73 | * minimum times in seconds (UINT32 each), for example: | |
74 | * `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial": | |
75 | * 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`. | |
76 | * - Any other unknown type: | |
77 | * An opaque binary string containing the RDATA as transported in the DNS | |
78 | * record. For forwards compatibility, you should not rely on this format | |
79 | * for unknown types. Future versions may add support for new types and | |
80 | * this may then parse the payload data appropriately - this will not be | |
81 | * considered a BC break. See the format definition of known types above | |
82 | * for more details. | |
83 | * | |
84 | * @var string|string[]|array | |
85 | */ | |
10 | 86 | public $data; |
11 | 87 | |
12 | 88 | public function __construct($name, $type, $class, $ttl = 0, $data = null) |
163 | 163 | $consumed += $rdLength; |
164 | 164 | |
165 | 165 | $rdata = inet_ntop($ip); |
166 | } | |
167 | ||
168 | if (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type) { | |
166 | } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) { | |
169 | 167 | list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); |
170 | 168 | |
171 | 169 | $rdata = implode('.', $bodyLabels); |
170 | } elseif (Message::TYPE_TXT === $type) { | |
171 | $rdata = array(); | |
172 | $remaining = $rdLength; | |
173 | while ($remaining) { | |
174 | $len = ord($message->data[$consumed]); | |
175 | $rdata[] = substr($message->data, $consumed + 1, $len); | |
176 | $consumed += $len + 1; | |
177 | $remaining -= $len + 1; | |
178 | } | |
179 | } elseif (Message::TYPE_MX === $type) { | |
180 | list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2))); | |
181 | list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed + 2); | |
182 | ||
183 | $rdata = array( | |
184 | 'priority' => $priority, | |
185 | 'target' => implode('.', $bodyLabels) | |
186 | ); | |
187 | } elseif (Message::TYPE_SRV === $type) { | |
188 | list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6))); | |
189 | list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed + 6); | |
190 | ||
191 | $rdata = array( | |
192 | 'priority' => $priority, | |
193 | 'weight' => $weight, | |
194 | 'port' => $port, | |
195 | 'target' => implode('.', $bodyLabels) | |
196 | ); | |
197 | } elseif (Message::TYPE_SOA === $type) { | |
198 | list($primaryLabels, $consumed) = $this->readLabels($message->data, $consumed); | |
199 | list($mailLabels, $consumed) = $this->readLabels($message->data, $consumed); | |
200 | list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20))); | |
201 | $consumed += 20; | |
202 | ||
203 | $rdata = array( | |
204 | 'mname' => implode('.', $primaryLabels), | |
205 | 'rname' => implode('.', $mailLabels), | |
206 | 'serial' => $serial, | |
207 | 'refresh' => $refresh, | |
208 | 'retry' => $retry, | |
209 | 'expire' => $expire, | |
210 | 'minimum' => $minimum | |
211 | ); | |
212 | } else { | |
213 | // unknown types simply parse rdata as an opaque binary string | |
214 | $rdata = substr($message->data, $consumed, $rdLength); | |
215 | $consumed += $rdLength; | |
172 | 216 | } |
173 | 217 | |
174 | 218 | $message->consumed = $consumed; |
10 | 10 | use React\Stream\DuplexResourceStream; |
11 | 11 | use React\Stream\Stream; |
12 | 12 | |
13 | /** | |
14 | * @deprecated unused, exists for BC only | |
15 | * @see UdpTransportExecutor | |
16 | */ | |
13 | 17 | class Executor implements ExecutorInterface |
14 | 18 | { |
15 | 19 | private $loop; |
6 | 6 | public $name; |
7 | 7 | public $type; |
8 | 8 | public $class; |
9 | ||
10 | /** | |
11 | * @deprecated still used internally for BC reasons, should not be used externally. | |
12 | */ | |
9 | 13 | public $currentTime; |
10 | 14 | |
11 | public function __construct($name, $type, $class, $currentTime) | |
15 | /** | |
16 | * @param string $name query name, i.e. hostname to look up | |
17 | * @param int $type query type, see Message::TYPE_* constants | |
18 | * @param int $class query class, see Message::CLASS_IN constant | |
19 | * @param int|null $currentTime (deprecated) still used internally, should not be passed explicitly anymore. | |
20 | */ | |
21 | public function __construct($name, $type, $class, $currentTime = null) | |
12 | 22 | { |
23 | if($currentTime === null) { | |
24 | $currentTime = time(); | |
25 | } | |
26 | ||
13 | 27 | $this->name = $name; |
14 | 28 | $this->type = $type; |
15 | 29 | $this->class = $class; |
5 | 5 | use React\Dns\Model\Message; |
6 | 6 | use React\Dns\Model\Record; |
7 | 7 | use React\Promise; |
8 | use React\Promise\PromiseInterface; | |
8 | 9 | |
10 | /** | |
11 | * Wraps an underlying cache interface and exposes only cached DNS data | |
12 | */ | |
9 | 13 | class RecordCache |
10 | 14 | { |
11 | 15 | private $cache; |
16 | 20 | $this->cache = $cache; |
17 | 21 | } |
18 | 22 | |
23 | /** | |
24 | * Looks up the cache if there's a cached answer for the given query | |
25 | * | |
26 | * @param Query $query | |
27 | * @return PromiseInterface Promise<Record[],mixed> resolves with array of Record objects on sucess | |
28 | * or rejects with mixed values when query is not cached already. | |
29 | */ | |
19 | 30 | public function lookup(Query $query) |
20 | 31 | { |
21 | 32 | $id = $this->serializeQueryToIdentity($query); |
25 | 36 | return $this->cache |
26 | 37 | ->get($id) |
27 | 38 | ->then(function ($value) use ($query, $expiredAt) { |
39 | // cache 0.5+ resolves with null on cache miss, return explicit cache miss here | |
40 | if ($value === null) { | |
41 | return Promise\reject(); | |
42 | } | |
43 | ||
44 | /* @var $recordBag RecordBag */ | |
28 | 45 | $recordBag = unserialize($value); |
29 | 46 | |
47 | // reject this cache hit if the query was started before the time we expired the cache? | |
48 | // todo: this is a legacy left over, this value is never actually set, so this never applies. | |
49 | // todo: this should probably validate the cache time instead. | |
30 | 50 | if (null !== $expiredAt && $expiredAt <= $query->currentTime) { |
31 | 51 | return Promise\reject(); |
32 | 52 | } |
35 | 55 | }); |
36 | 56 | } |
37 | 57 | |
58 | /** | |
59 | * Stores all records from this response message in the cache | |
60 | * | |
61 | * @param int $currentTime | |
62 | * @param Message $message | |
63 | * @uses self::storeRecord() | |
64 | */ | |
38 | 65 | public function storeResponseMessage($currentTime, Message $message) |
39 | 66 | { |
40 | 67 | foreach ($message->answers as $record) { |
42 | 69 | } |
43 | 70 | } |
44 | 71 | |
72 | /** | |
73 | * Stores a single record from a response message in the cache | |
74 | * | |
75 | * @param int $currentTime | |
76 | * @param Record $record | |
77 | */ | |
45 | 78 | public function storeRecord($currentTime, Record $record) |
46 | 79 | { |
47 | 80 | $id = $this->serializeRecordToIdentity($record); |
52 | 85 | ->get($id) |
53 | 86 | ->then( |
54 | 87 | function ($value) { |
88 | if ($value === null) { | |
89 | // cache 0.5+ cache miss resolves with null, return empty bag here | |
90 | return new RecordBag(); | |
91 | } | |
92 | ||
93 | // reuse existing bag on cache hit to append new record to it | |
55 | 94 | return unserialize($value); |
56 | 95 | }, |
57 | 96 | function ($e) { |
97 | // legacy cache < 0.5 cache miss rejects promise, return empty bag here | |
58 | 98 | return new RecordBag(); |
59 | 99 | } |
60 | 100 | ) |
61 | ->then(function ($recordBag) use ($id, $currentTime, $record, $cache) { | |
101 | ->then(function (RecordBag $recordBag) use ($id, $currentTime, $record, $cache) { | |
102 | // add a record to the existing (possibly empty) record bag and save to cache | |
62 | 103 | $recordBag->set($currentTime, $record); |
63 | 104 | $cache->set($id, serialize($recordBag)); |
64 | 105 | }); |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Protocol\BinaryDumper; | |
6 | use React\Dns\Protocol\Parser; | |
7 | use React\EventLoop\LoopInterface; | |
8 | use React\Promise\Deferred; | |
9 | ||
10 | /** | |
11 | * Send DNS queries over a UDP transport. | |
12 | * | |
13 | * This is the main class that sends a DNS query to your DNS server and is used | |
14 | * internally by the `Resolver` for the actual message transport. | |
15 | * | |
16 | * For more advanced usages one can utilize this class directly. | |
17 | * The following example looks up the `IPv6` address for `igor.io`. | |
18 | * | |
19 | * ```php | |
20 | * $loop = Factory::create(); | |
21 | * $executor = new UdpTransportExecutor($loop); | |
22 | * | |
23 | * $executor->query( | |
24 | * '8.8.8.8:53', | |
25 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
26 | * )->then(function (Message $message) { | |
27 | * foreach ($message->answers as $answer) { | |
28 | * echo 'IPv6: ' . $answer->data . PHP_EOL; | |
29 | * } | |
30 | * }, 'printf'); | |
31 | * | |
32 | * $loop->run(); | |
33 | * ``` | |
34 | * | |
35 | * See also the [fourth example](examples). | |
36 | * | |
37 | * Note that this executor does not implement a timeout, so you will very likely | |
38 | * want to use this in combination with a `TimeoutExecutor` like this: | |
39 | * | |
40 | * ```php | |
41 | * $executor = new TimeoutExecutor( | |
42 | * new UdpTransportExecutor($loop), | |
43 | * 3.0, | |
44 | * $loop | |
45 | * ); | |
46 | * ``` | |
47 | * | |
48 | * Also note that this executor uses an unreliable UDP transport and that it | |
49 | * does not implement any retry logic, so you will likely want to use this in | |
50 | * combination with a `RetryExecutor` like this: | |
51 | * | |
52 | * ```php | |
53 | * $executor = new RetryExecutor( | |
54 | * new TimeoutExecutor( | |
55 | * new UdpTransportExecutor($loop), | |
56 | * 3.0, | |
57 | * $loop | |
58 | * ) | |
59 | * ); | |
60 | * ``` | |
61 | * | |
62 | * > Internally, this class uses PHP's UDP sockets and does not take advantage | |
63 | * of [react/datagram](https://github.com/reactphp/datagram) purely for | |
64 | * organizational reasons to avoid a cyclic dependency between the two | |
65 | * packages. Higher-level components should take advantage of the Datagram | |
66 | * component instead of reimplementing this socket logic from scratch. | |
67 | */ | |
68 | class UdpTransportExecutor implements ExecutorInterface | |
69 | { | |
70 | private $loop; | |
71 | private $parser; | |
72 | private $dumper; | |
73 | ||
74 | /** | |
75 | * @param LoopInterface $loop | |
76 | * @param null|Parser $parser optional/advanced: DNS protocol parser to use | |
77 | * @param null|BinaryDumper $dumper optional/advanced: DNS protocol dumper to use | |
78 | */ | |
79 | public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null) | |
80 | { | |
81 | if ($parser === null) { | |
82 | $parser = new Parser(); | |
83 | } | |
84 | if ($dumper === null) { | |
85 | $dumper = new BinaryDumper(); | |
86 | } | |
87 | ||
88 | $this->loop = $loop; | |
89 | $this->parser = $parser; | |
90 | $this->dumper = $dumper; | |
91 | } | |
92 | ||
93 | public function query($nameserver, Query $query) | |
94 | { | |
95 | $request = Message::createRequestForQuery($query); | |
96 | ||
97 | $queryData = $this->dumper->toBinary($request); | |
98 | if (isset($queryData[512])) { | |
99 | return \React\Promise\reject(new \RuntimeException( | |
100 | 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport' | |
101 | )); | |
102 | } | |
103 | ||
104 | // UDP connections are instant, so try connection without a loop or timeout | |
105 | $socket = @\stream_socket_client("udp://$nameserver", $errno, $errstr, 0); | |
106 | if ($socket === false) { | |
107 | return \React\Promise\reject(new \RuntimeException( | |
108 | 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', | |
109 | $errno | |
110 | )); | |
111 | } | |
112 | ||
113 | // set socket to non-blocking and immediately try to send (fill write buffer) | |
114 | \stream_set_blocking($socket, false); | |
115 | \fwrite($socket, $queryData); | |
116 | ||
117 | $loop = $this->loop; | |
118 | $deferred = new Deferred(function () use ($loop, $socket, $query) { | |
119 | // cancellation should remove socket from loop and close socket | |
120 | $loop->removeReadStream($socket); | |
121 | \fclose($socket); | |
122 | ||
123 | throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled'); | |
124 | }); | |
125 | ||
126 | $parser = $this->parser; | |
127 | $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request) { | |
128 | // try to read a single data packet from the DNS server | |
129 | // ignoring any errors, this is uses UDP packets and not a stream of data | |
130 | $data = @\fread($socket, 512); | |
131 | ||
132 | try { | |
133 | $response = $parser->parseMessage($data); | |
134 | } catch (\Exception $e) { | |
135 | // ignore and await next if we received an invalid message from remote server | |
136 | // this may as well be a fake response from an attacker (possible DOS) | |
137 | return; | |
138 | } | |
139 | ||
140 | // ignore and await next if we received an unexpected response ID | |
141 | // this may as well be a fake response from an attacker (possible cache poisoning) | |
142 | if ($response->getId() !== $request->getId()) { | |
143 | return; | |
144 | } | |
145 | ||
146 | // we only react to the first valid message, so remove socket from loop and close | |
147 | $loop->removeReadStream($socket); | |
148 | \fclose($socket); | |
149 | ||
150 | if ($response->header->isTruncated()) { | |
151 | $deferred->reject(new \RuntimeException('DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported')); | |
152 | return; | |
153 | } | |
154 | ||
155 | $deferred->resolve($response); | |
156 | }); | |
157 | ||
158 | return $deferred->promise(); | |
159 | } | |
160 | } |
4 | 4 | use React\Cache\ArrayCache; |
5 | 5 | use React\Cache\CacheInterface; |
6 | 6 | use React\Dns\Config\HostsFile; |
7 | use React\Dns\Protocol\Parser; | |
8 | use React\Dns\Protocol\BinaryDumper; | |
9 | 7 | use React\Dns\Query\CachedExecutor; |
10 | use React\Dns\Query\Executor; | |
11 | 8 | use React\Dns\Query\ExecutorInterface; |
12 | 9 | use React\Dns\Query\HostsFileExecutor; |
13 | 10 | use React\Dns\Query\RecordCache; |
14 | 11 | use React\Dns\Query\RetryExecutor; |
15 | 12 | use React\Dns\Query\TimeoutExecutor; |
13 | use React\Dns\Query\UdpTransportExecutor; | |
16 | 14 | use React\EventLoop\LoopInterface; |
17 | 15 | |
18 | 16 | class Factory |
70 | 68 | protected function createExecutor(LoopInterface $loop) |
71 | 69 | { |
72 | 70 | return new TimeoutExecutor( |
73 | new Executor($loop, new Parser(), new BinaryDumper(), null), | |
71 | new UdpTransportExecutor($loop), | |
74 | 72 | 5.0, |
75 | 73 | $loop |
76 | 74 | ); |
1 | 1 | |
2 | 2 | namespace React\Dns\Resolver; |
3 | 3 | |
4 | use React\Dns\Model\Message; | |
4 | 5 | use React\Dns\Query\ExecutorInterface; |
5 | 6 | use React\Dns\Query\Query; |
6 | 7 | use React\Dns\RecordNotFoundException; |
7 | use React\Dns\Model\Message; | |
8 | use React\Promise\PromiseInterface; | |
8 | 9 | |
9 | 10 | class Resolver |
10 | 11 | { |
17 | 18 | $this->executor = $executor; |
18 | 19 | } |
19 | 20 | |
21 | /** | |
22 | * Resolves the given $domain name to a single IPv4 address (type `A` query). | |
23 | * | |
24 | * ```php | |
25 | * $resolver->resolve('reactphp.org')->then(function ($ip) { | |
26 | * echo 'IP for reactphp.org is ' . $ip . PHP_EOL; | |
27 | * }); | |
28 | * ``` | |
29 | * | |
30 | * This is one of the main methods in this package. It sends a DNS query | |
31 | * for the given $domain name to your DNS server and returns a single IP | |
32 | * address on success. | |
33 | * | |
34 | * If the DNS server sends a DNS response message that contains more than | |
35 | * one IP address for this query, it will randomly pick one of the IP | |
36 | * addresses from the response. If you want the full list of IP addresses | |
37 | * or want to send a different type of query, you should use the | |
38 | * [`resolveAll()`](#resolveall) method instead. | |
39 | * | |
40 | * If the DNS server sends a DNS response message that indicates an error | |
41 | * code, this method will reject with a `RecordNotFoundException`. Its | |
42 | * message and code can be used to check for the response code. | |
43 | * | |
44 | * If the DNS communication fails and the server does not respond with a | |
45 | * valid response message, this message will reject with an `Exception`. | |
46 | * | |
47 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
48 | * | |
49 | * ```php | |
50 | * $promise = $resolver->resolve('reactphp.org'); | |
51 | * | |
52 | * $promise->cancel(); | |
53 | * ``` | |
54 | * | |
55 | * @param string $domain | |
56 | * @return PromiseInterface Returns a promise which resolves with a single IP address on success or | |
57 | * rejects with an Exception on error. | |
58 | */ | |
20 | 59 | public function resolve($domain) |
21 | 60 | { |
22 | $query = new Query($domain, Message::TYPE_A, Message::CLASS_IN, time()); | |
61 | return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) { | |
62 | return $ips[array_rand($ips)]; | |
63 | }); | |
64 | } | |
65 | ||
66 | /** | |
67 | * Resolves all record values for the given $domain name and query $type. | |
68 | * | |
69 | * ```php | |
70 | * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { | |
71 | * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
72 | * }); | |
73 | * | |
74 | * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { | |
75 | * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
76 | * }); | |
77 | * ``` | |
78 | * | |
79 | * This is one of the main methods in this package. It sends a DNS query | |
80 | * for the given $domain name to your DNS server and returns a list with all | |
81 | * record values on success. | |
82 | * | |
83 | * If the DNS server sends a DNS response message that contains one or more | |
84 | * records for this query, it will return a list with all record values | |
85 | * from the response. You can use the `Message::TYPE_*` constants to control | |
86 | * which type of query will be sent. Note that this method always returns a | |
87 | * list of record values, but each record value type depends on the query | |
88 | * type. For example, it returns the IPv4 addresses for type `A` queries, | |
89 | * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, | |
90 | * `CNAME` and `PTR` queries and structured data for other queries. See also | |
91 | * the `Record` documentation for more details. | |
92 | * | |
93 | * If the DNS server sends a DNS response message that indicates an error | |
94 | * code, this method will reject with a `RecordNotFoundException`. Its | |
95 | * message and code can be used to check for the response code. | |
96 | * | |
97 | * If the DNS communication fails and the server does not respond with a | |
98 | * valid response message, this message will reject with an `Exception`. | |
99 | * | |
100 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
101 | * | |
102 | * ```php | |
103 | * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); | |
104 | * | |
105 | * $promise->cancel(); | |
106 | * ``` | |
107 | * | |
108 | * @param string $domain | |
109 | * @return PromiseInterface Returns a promise which resolves with all record values on success or | |
110 | * rejects with an Exception on error. | |
111 | */ | |
112 | public function resolveAll($domain, $type) | |
113 | { | |
114 | $query = new Query($domain, $type, Message::CLASS_IN); | |
23 | 115 | $that = $this; |
24 | 116 | |
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 | ||
117 | return $this->executor->query( | |
118 | $this->nameserver, | |
119 | $query | |
120 | )->then(function (Message $response) use ($query, $that) { | |
121 | return $that->extractValues($query, $response); | |
122 | }); | |
123 | } | |
124 | ||
125 | /** | |
126 | * @deprecated unused, exists for BC only | |
127 | */ | |
32 | 128 | public function extractAddress(Query $query, Message $response) |
33 | 129 | { |
130 | $addresses = $this->extractValues($query, $response); | |
131 | ||
132 | return $addresses[array_rand($addresses)]; | |
133 | } | |
134 | ||
135 | /** | |
136 | * [Internal] extract all resource record values from response for this query | |
137 | * | |
138 | * @param Query $query | |
139 | * @param Message $response | |
140 | * @return array | |
141 | * @throws RecordNotFoundException when response indicates an error or contains no data | |
142 | * @internal | |
143 | */ | |
144 | public function extractValues(Query $query, Message $response) | |
145 | { | |
146 | // reject if response code indicates this is an error response message | |
147 | $code = $response->getResponseCode(); | |
148 | if ($code !== Message::RCODE_OK) { | |
149 | switch ($code) { | |
150 | case Message::RCODE_FORMAT_ERROR: | |
151 | $message = 'Format Error'; | |
152 | break; | |
153 | case Message::RCODE_SERVER_FAILURE: | |
154 | $message = 'Server Failure'; | |
155 | break; | |
156 | case Message::RCODE_NAME_ERROR: | |
157 | $message = 'Non-Existent Domain / NXDOMAIN'; | |
158 | break; | |
159 | case Message::RCODE_NOT_IMPLEMENTED: | |
160 | $message = 'Not Implemented'; | |
161 | break; | |
162 | case Message::RCODE_REFUSED: | |
163 | $message = 'Refused'; | |
164 | break; | |
165 | default: | |
166 | $message = 'Unknown error response code ' . $code; | |
167 | } | |
168 | throw new RecordNotFoundException( | |
169 | 'DNS query for ' . $query->name . ' returned an error response (' . $message . ')', | |
170 | $code | |
171 | ); | |
172 | } | |
173 | ||
34 | 174 | $answers = $response->answers; |
35 | ||
36 | $addresses = $this->resolveAliases($answers, $query->name); | |
37 | ||
175 | $addresses = $this->valuesByNameAndType($answers, $query->name, $query->type); | |
176 | ||
177 | // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found) | |
38 | 178 | 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 | ||
179 | throw new RecordNotFoundException( | |
180 | 'DNS query for ' . $query->name . ' did not return a valid answer (NOERROR / NODATA)' | |
181 | ); | |
182 | } | |
183 | ||
184 | return array_values($addresses); | |
185 | } | |
186 | ||
187 | /** | |
188 | * @deprecated unused, exists for BC only | |
189 | */ | |
47 | 190 | public function resolveAliases(array $answers, $name) |
48 | 191 | { |
192 | return $this->valuesByNameAndType($answers, $name, Message::TYPE_A); | |
193 | } | |
194 | ||
195 | /** | |
196 | * @param \React\Dns\Model\Record[] $answers | |
197 | * @param string $name | |
198 | * @param int $type | |
199 | * @return array | |
200 | */ | |
201 | private function valuesByNameAndType(array $answers, $name, $type) | |
202 | { | |
203 | // return all record values for this name and type (if any) | |
49 | 204 | $named = $this->filterByName($answers, $name); |
50 | $aRecords = $this->filterByType($named, Message::TYPE_A); | |
205 | $records = $this->filterByType($named, $type); | |
206 | if ($records) { | |
207 | return $this->mapRecordData($records); | |
208 | } | |
209 | ||
210 | // no matching records found? check if there are any matching CNAMEs instead | |
51 | 211 | $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME); |
52 | ||
53 | if ($aRecords) { | |
54 | return $this->mapRecordData($aRecords); | |
55 | } | |
56 | ||
57 | 212 | if ($cnameRecords) { |
58 | $aRecords = array(); | |
59 | ||
60 | 213 | $cnames = $this->mapRecordData($cnameRecords); |
61 | 214 | foreach ($cnames as $cname) { |
62 | $targets = $this->filterByName($answers, $cname); | |
63 | $aRecords = array_merge( | |
64 | $aRecords, | |
65 | $this->resolveAliases($answers, $cname) | |
215 | $records = array_merge( | |
216 | $records, | |
217 | $this->valuesByNameAndType($answers, $cname, $type) | |
66 | 218 | ); |
67 | 219 | } |
68 | ||
69 | return $aRecords; | |
70 | } | |
71 | ||
72 | return array(); | |
220 | } | |
221 | ||
222 | return $records; | |
73 | 223 | } |
74 | 224 | |
75 | 225 | private function filterByName(array $answers, $name) |
1 | 1 | |
2 | 2 | namespace React\Tests\Dns; |
3 | 3 | |
4 | use React\Tests\Dns\TestCase; | |
5 | 4 | use React\EventLoop\Factory as LoopFactory; |
6 | use React\Dns\Resolver\Resolver; | |
7 | 5 | use React\Dns\Resolver\Factory; |
6 | use React\Dns\RecordNotFoundException; | |
7 | use React\Dns\Model\Message; | |
8 | 8 | |
9 | 9 | class FunctionalTest extends TestCase |
10 | 10 | { |
20 | 20 | { |
21 | 21 | $promise = $this->resolver->resolve('localhost'); |
22 | 22 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); |
23 | ||
24 | $this->loop->run(); | |
25 | } | |
26 | ||
27 | public function testResolveAllLocalhostResolvesWithArray() | |
28 | { | |
29 | $promise = $this->resolver->resolveAll('localhost', Message::TYPE_A); | |
30 | $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); | |
23 | 31 | |
24 | 32 | $this->loop->run(); |
25 | 33 | } |
40 | 48 | */ |
41 | 49 | public function testResolveInvalidRejects() |
42 | 50 | { |
51 | $ex = $this->callback(function ($param) { | |
52 | return ($param instanceof RecordNotFoundException && $param->getCode() === Message::RCODE_NAME_ERROR); | |
53 | }); | |
54 | ||
43 | 55 | $promise = $this->resolver->resolve('example.invalid'); |
44 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); | |
56 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex)); | |
45 | 57 | |
46 | 58 | $this->loop->run(); |
47 | 59 | } |
48 | 60 | |
49 | 61 | public function testResolveCancelledRejectsImmediately() |
50 | 62 | { |
63 | $ex = $this->callback(function ($param) { | |
64 | return ($param instanceof \RuntimeException && $param->getMessage() === 'DNS query for google.com has been cancelled'); | |
65 | }); | |
66 | ||
51 | 67 | $promise = $this->resolver->resolve('google.com'); |
52 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); | |
68 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex)); | |
53 | 69 | $promise->cancel(); |
54 | 70 | |
55 | 71 | $time = microtime(true); |
25 | 25 | $this->assertFalse($request->header->isQuery()); |
26 | 26 | $this->assertTrue($request->header->isResponse()); |
27 | 27 | $this->assertEquals(0, $request->header->get('anCount')); |
28 | $this->assertEquals(Message::RCODE_OK, $request->getResponseCode()); | |
28 | 29 | } |
29 | 30 | } |
156 | 156 | $this->assertSame('178.79.169.131', $response->answers[0]->data); |
157 | 157 | } |
158 | 158 | |
159 | public function testParseAnswerWithUnknownType() | |
160 | { | |
161 | $data = ""; | |
162 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
163 | $data .= "23 28 00 01"; // answer: type 9000, class IN | |
164 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
165 | $data .= "00 05"; // answer: rdlength 5 | |
166 | $data .= "68 65 6c 6c 6f"; // answer: rdata "hello" | |
167 | ||
168 | $data = $this->convertTcpDumpToBinary($data); | |
169 | ||
170 | $response = new Message(); | |
171 | $response->header->set('anCount', 1); | |
172 | $response->data = $data; | |
173 | ||
174 | $this->parser->parseAnswer($response); | |
175 | ||
176 | $this->assertCount(1, $response->answers); | |
177 | $this->assertSame('igor.io', $response->answers[0]->name); | |
178 | $this->assertSame(9000, $response->answers[0]->type); | |
179 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
180 | $this->assertSame(86400, $response->answers[0]->ttl); | |
181 | $this->assertSame('hello', $response->answers[0]->data); | |
182 | } | |
183 | ||
159 | 184 | public function testParseResponseWithCnameAndOffsetPointers() |
160 | 185 | { |
161 | 186 | $data = ""; |
230 | 255 | $this->assertSame('2a00:1450:4009:809::200e', $response->answers[0]->data); |
231 | 256 | } |
232 | 257 | |
258 | public function testParseTXTResponse() | |
259 | { | |
260 | $data = ""; | |
261 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
262 | $data .= "00 10 00 01"; // answer: type TXT, class IN | |
263 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
264 | $data .= "00 06"; // answer: rdlength 6 | |
265 | $data .= "05 68 65 6c 6c 6f"; // answer: rdata length 5: hello | |
266 | ||
267 | $data = $this->convertTcpDumpToBinary($data); | |
268 | ||
269 | $response = new Message(); | |
270 | $response->header->set('anCount', 1); | |
271 | $response->data = $data; | |
272 | ||
273 | $this->parser->parseAnswer($response); | |
274 | ||
275 | $this->assertCount(1, $response->answers); | |
276 | $this->assertSame('igor.io', $response->answers[0]->name); | |
277 | $this->assertSame(Message::TYPE_TXT, $response->answers[0]->type); | |
278 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
279 | $this->assertSame(86400, $response->answers[0]->ttl); | |
280 | $this->assertSame(array('hello'), $response->answers[0]->data); | |
281 | } | |
282 | ||
283 | public function testParseTXTResponseMultiple() | |
284 | { | |
285 | $data = ""; | |
286 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
287 | $data .= "00 10 00 01"; // answer: type TXT, class IN | |
288 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
289 | $data .= "00 0C"; // answer: rdlength 12 | |
290 | $data .= "05 68 65 6c 6c 6f 05 77 6f 72 6c 64"; // answer: rdata length 5: hello, length 5: world | |
291 | ||
292 | $data = $this->convertTcpDumpToBinary($data); | |
293 | ||
294 | $response = new Message(); | |
295 | $response->header->set('anCount', 1); | |
296 | $response->data = $data; | |
297 | ||
298 | $this->parser->parseAnswer($response); | |
299 | ||
300 | $this->assertCount(1, $response->answers); | |
301 | $this->assertSame('igor.io', $response->answers[0]->name); | |
302 | $this->assertSame(Message::TYPE_TXT, $response->answers[0]->type); | |
303 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
304 | $this->assertSame(86400, $response->answers[0]->ttl); | |
305 | $this->assertSame(array('hello', 'world'), $response->answers[0]->data); | |
306 | } | |
307 | ||
308 | public function testParseMXResponse() | |
309 | { | |
310 | $data = ""; | |
311 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
312 | $data .= "00 0f 00 01"; // answer: type MX, class IN | |
313 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
314 | $data .= "00 09"; // answer: rdlength 9 | |
315 | $data .= "00 0a 05 68 65 6c 6c 6f 00"; // answer: rdata priority 10: hello | |
316 | ||
317 | $data = $this->convertTcpDumpToBinary($data); | |
318 | ||
319 | $response = new Message(); | |
320 | $response->header->set('anCount', 1); | |
321 | $response->data = $data; | |
322 | ||
323 | $this->parser->parseAnswer($response); | |
324 | ||
325 | $this->assertCount(1, $response->answers); | |
326 | $this->assertSame('igor.io', $response->answers[0]->name); | |
327 | $this->assertSame(Message::TYPE_MX, $response->answers[0]->type); | |
328 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
329 | $this->assertSame(86400, $response->answers[0]->ttl); | |
330 | $this->assertSame(array('priority' => 10, 'target' => 'hello'), $response->answers[0]->data); | |
331 | } | |
332 | ||
333 | public function testParseSRVResponse() | |
334 | { | |
335 | $data = ""; | |
336 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
337 | $data .= "00 21 00 01"; // answer: type SRV, class IN | |
338 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
339 | $data .= "00 0C"; // answer: rdlength 12 | |
340 | $data .= "00 0a 00 14 1F 90 04 74 65 73 74 00"; // answer: rdata priority 10, weight 20, port 8080 test | |
341 | ||
342 | $data = $this->convertTcpDumpToBinary($data); | |
343 | ||
344 | $response = new Message(); | |
345 | $response->header->set('anCount', 1); | |
346 | $response->data = $data; | |
347 | ||
348 | $this->parser->parseAnswer($response); | |
349 | ||
350 | $this->assertCount(1, $response->answers); | |
351 | $this->assertSame('igor.io', $response->answers[0]->name); | |
352 | $this->assertSame(Message::TYPE_SRV, $response->answers[0]->type); | |
353 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
354 | $this->assertSame(86400, $response->answers[0]->ttl); | |
355 | $this->assertSame( | |
356 | array( | |
357 | 'priority' => 10, | |
358 | 'weight' => 20, | |
359 | 'port' => 8080, | |
360 | 'target' => 'test' | |
361 | ), | |
362 | $response->answers[0]->data | |
363 | ); | |
364 | } | |
365 | ||
233 | 366 | public function testParseResponseWithTwoAnswers() |
234 | 367 | { |
235 | 368 | $data = ""; |
272 | 405 | $this->assertSame('193.223.78.152', $response->answers[1]->data); |
273 | 406 | } |
274 | 407 | |
408 | public function testParseNSResponse() | |
409 | { | |
410 | $data = ""; | |
411 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
412 | $data .= "00 02 00 01"; // answer: type NS, class IN | |
413 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
414 | $data .= "00 07"; // answer: rdlength 7 | |
415 | $data .= "05 68 65 6c 6c 6f 00"; // answer: rdata hello | |
416 | ||
417 | $data = $this->convertTcpDumpToBinary($data); | |
418 | ||
419 | $response = new Message(); | |
420 | $response->header->set('anCount', 1); | |
421 | $response->data = $data; | |
422 | ||
423 | $this->parser->parseAnswer($response); | |
424 | ||
425 | $this->assertCount(1, $response->answers); | |
426 | $this->assertSame('igor.io', $response->answers[0]->name); | |
427 | $this->assertSame(Message::TYPE_NS, $response->answers[0]->type); | |
428 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
429 | $this->assertSame(86400, $response->answers[0]->ttl); | |
430 | $this->assertSame('hello', $response->answers[0]->data); | |
431 | } | |
432 | ||
433 | public function testParseSOAResponse() | |
434 | { | |
435 | $data = ""; | |
436 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
437 | $data .= "00 06 00 01"; // answer: type SOA, class IN | |
438 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
439 | $data .= "00 07"; // answer: rdlength 7 | |
440 | $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) | |
441 | $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) | |
442 | $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600 | |
443 | $data .= "00 09 3a 80 00 00 0e 10"; // answer: 605800, 3600 | |
444 | ||
445 | $data = $this->convertTcpDumpToBinary($data); | |
446 | ||
447 | $response = new Message(); | |
448 | $response->header->set('anCount', 1); | |
449 | $response->data = $data; | |
450 | ||
451 | $this->parser->parseAnswer($response); | |
452 | ||
453 | $this->assertCount(1, $response->answers); | |
454 | $this->assertSame('igor.io', $response->answers[0]->name); | |
455 | $this->assertSame(Message::TYPE_SOA, $response->answers[0]->type); | |
456 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
457 | $this->assertSame(86400, $response->answers[0]->ttl); | |
458 | $this->assertSame( | |
459 | array( | |
460 | 'mname' => 'ns.hello', | |
461 | 'rname' => 'e.hello', | |
462 | 'serial' => 2018060501, | |
463 | 'refresh' => 10800, | |
464 | 'retry' => 3600, | |
465 | 'expire' => 604800, | |
466 | 'minimum' => 3600 | |
467 | ), | |
468 | $response->answers[0]->data | |
469 | ); | |
470 | } | |
471 | ||
275 | 472 | public function testParsePTRResponse() |
276 | 473 | { |
277 | 474 | $data = ""; |
8 | 8 | use React\Dns\Query\RecordCache; |
9 | 9 | use React\Dns\Query\Query; |
10 | 10 | use React\Promise\PromiseInterface; |
11 | use React\Promise\Promise; | |
11 | 12 | |
12 | 13 | class RecordCacheTest extends TestCase |
13 | 14 | { |
14 | 15 | /** |
15 | * @covers React\Dns\Query\RecordCache | |
16 | * @test | |
17 | */ | |
18 | public function lookupOnEmptyCacheShouldReturnNull() | |
16 | * @covers React\Dns\Query\RecordCache | |
17 | * @test | |
18 | */ | |
19 | public function lookupOnNewCacheMissShouldReturnNull() | |
19 | 20 | { |
20 | 21 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); |
21 | 22 | |
22 | $cache = new RecordCache(new ArrayCache()); | |
23 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
24 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); | |
25 | ||
26 | $cache = new RecordCache($base); | |
23 | 27 | $promise = $cache->lookup($query); |
24 | 28 | |
25 | 29 | $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); |
30 | } | |
31 | ||
32 | /** | |
33 | * @covers React\Dns\Query\RecordCache | |
34 | * @test | |
35 | */ | |
36 | public function lookupOnLegacyCacheMissShouldReturnNull() | |
37 | { | |
38 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
39 | ||
40 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
41 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\reject()); | |
42 | ||
43 | $cache = new RecordCache($base); | |
44 | $promise = $cache->lookup($query); | |
45 | ||
46 | $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); | |
47 | } | |
48 | ||
49 | /** | |
50 | * @covers React\Dns\Query\RecordCache | |
51 | * @test | |
52 | */ | |
53 | public function storeRecordPendingCacheDoesNotSetCache() | |
54 | { | |
55 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
56 | $pending = new Promise(function () { }); | |
57 | ||
58 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
59 | $base->expects($this->once())->method('get')->willReturn($pending); | |
60 | $base->expects($this->never())->method('set'); | |
61 | ||
62 | $cache = new RecordCache($base); | |
63 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
64 | } | |
65 | ||
66 | /** | |
67 | * @covers React\Dns\Query\RecordCache | |
68 | * @test | |
69 | */ | |
70 | public function storeRecordOnNewCacheMissSetsCache() | |
71 | { | |
72 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
73 | ||
74 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
75 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); | |
76 | $base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string')); | |
77 | ||
78 | $cache = new RecordCache($base); | |
79 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
80 | } | |
81 | ||
82 | /** | |
83 | * @covers React\Dns\Query\RecordCache | |
84 | * @test | |
85 | */ | |
86 | public function storeRecordOnOldCacheMissSetsCache() | |
87 | { | |
88 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
89 | ||
90 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
91 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\reject()); | |
92 | $base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string')); | |
93 | ||
94 | $cache = new RecordCache($base); | |
95 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
26 | 96 | } |
27 | 97 | |
28 | 98 | /** |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Protocol\BinaryDumper; | |
6 | use React\Dns\Protocol\Parser; | |
7 | use React\Dns\Query\Query; | |
8 | use React\Dns\Query\UdpTransportExecutor; | |
9 | use React\EventLoop\Factory; | |
10 | use React\Tests\Dns\TestCase; | |
11 | ||
12 | class UdpTransportExecutorTest extends TestCase | |
13 | { | |
14 | public function testQueryRejectsIfMessageExceedsUdpSize() | |
15 | { | |
16 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
17 | $loop->expects($this->never())->method('addReadStream'); | |
18 | ||
19 | $dumper = $this->getMockBuilder('React\Dns\Protocol\BinaryDumper')->getMock(); | |
20 | $dumper->expects($this->once())->method('toBinary')->willReturn(str_repeat('.', 513)); | |
21 | ||
22 | $executor = new UdpTransportExecutor($loop, null, $dumper); | |
23 | ||
24 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
25 | $promise = $executor->query('8.8.8.8:53', $query); | |
26 | ||
27 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
28 | $promise->then(null, $this->expectCallableOnce()); | |
29 | } | |
30 | ||
31 | public function testQueryRejectsIfServerConnectionFails() | |
32 | { | |
33 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
34 | $loop->expects($this->never())->method('addReadStream'); | |
35 | ||
36 | $executor = new UdpTransportExecutor($loop); | |
37 | ||
38 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
39 | $promise = $executor->query('///', $query); | |
40 | ||
41 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
42 | $promise->then(null, $this->expectCallableOnce()); | |
43 | } | |
44 | ||
45 | /** | |
46 | * @group internet | |
47 | */ | |
48 | public function testQueryRejectsOnCancellation() | |
49 | { | |
50 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
51 | $loop->expects($this->once())->method('addReadStream'); | |
52 | $loop->expects($this->once())->method('removeReadStream'); | |
53 | ||
54 | $executor = new UdpTransportExecutor($loop); | |
55 | ||
56 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
57 | $promise = $executor->query('8.8.8.8:53', $query); | |
58 | $promise->cancel(); | |
59 | ||
60 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
61 | $promise->then(null, $this->expectCallableOnce()); | |
62 | } | |
63 | ||
64 | public function testQueryKeepsPendingIfServerRejectsNetworkPacket() | |
65 | { | |
66 | $loop = Factory::create(); | |
67 | ||
68 | $executor = new UdpTransportExecutor($loop); | |
69 | ||
70 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
71 | ||
72 | $wait = true; | |
73 | $promise = $executor->query('127.0.0.1:1', $query)->then( | |
74 | null, | |
75 | function ($e) use (&$wait) { | |
76 | $wait = false; | |
77 | throw $e; | |
78 | } | |
79 | ); | |
80 | ||
81 | \Clue\React\Block\sleep(0.2, $loop); | |
82 | $this->assertTrue($wait); | |
83 | } | |
84 | ||
85 | public function testQueryKeepsPendingIfServerSendInvalidMessage() | |
86 | { | |
87 | $loop = Factory::create(); | |
88 | ||
89 | $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); | |
90 | $loop->addReadStream($server, function ($server) { | |
91 | $data = stream_socket_recvfrom($server, 512, 0, $peer); | |
92 | stream_socket_sendto($server, 'invalid', 0, $peer); | |
93 | }); | |
94 | ||
95 | $address = stream_socket_get_name($server, false); | |
96 | $executor = new UdpTransportExecutor($loop); | |
97 | ||
98 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
99 | ||
100 | $wait = true; | |
101 | $promise = $executor->query($address, $query)->then( | |
102 | null, | |
103 | function ($e) use (&$wait) { | |
104 | $wait = false; | |
105 | throw $e; | |
106 | } | |
107 | ); | |
108 | ||
109 | \Clue\React\Block\sleep(0.2, $loop); | |
110 | $this->assertTrue($wait); | |
111 | } | |
112 | ||
113 | public function testQueryKeepsPendingIfServerSendInvalidId() | |
114 | { | |
115 | $parser = new Parser(); | |
116 | $dumper = new BinaryDumper(); | |
117 | ||
118 | $loop = Factory::create(); | |
119 | ||
120 | $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); | |
121 | $loop->addReadStream($server, function ($server) use ($parser, $dumper) { | |
122 | $data = stream_socket_recvfrom($server, 512, 0, $peer); | |
123 | ||
124 | $message = $parser->parseMessage($data); | |
125 | $message->header->set('id', 0); | |
126 | ||
127 | stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); | |
128 | }); | |
129 | ||
130 | $address = stream_socket_get_name($server, false); | |
131 | $executor = new UdpTransportExecutor($loop, $parser, $dumper); | |
132 | ||
133 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
134 | ||
135 | $wait = true; | |
136 | $promise = $executor->query($address, $query)->then( | |
137 | null, | |
138 | function ($e) use (&$wait) { | |
139 | $wait = false; | |
140 | throw $e; | |
141 | } | |
142 | ); | |
143 | ||
144 | \Clue\React\Block\sleep(0.2, $loop); | |
145 | $this->assertTrue($wait); | |
146 | } | |
147 | ||
148 | public function testQueryRejectsIfServerSendsTruncatedResponse() | |
149 | { | |
150 | $parser = new Parser(); | |
151 | $dumper = new BinaryDumper(); | |
152 | ||
153 | $loop = Factory::create(); | |
154 | ||
155 | $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); | |
156 | $loop->addReadStream($server, function ($server) use ($parser, $dumper) { | |
157 | $data = stream_socket_recvfrom($server, 512, 0, $peer); | |
158 | ||
159 | $message = $parser->parseMessage($data); | |
160 | $message->header->set('tc', 1); | |
161 | ||
162 | stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); | |
163 | }); | |
164 | ||
165 | $address = stream_socket_get_name($server, false); | |
166 | $executor = new UdpTransportExecutor($loop, $parser, $dumper); | |
167 | ||
168 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
169 | ||
170 | $wait = true; | |
171 | $promise = $executor->query($address, $query)->then( | |
172 | null, | |
173 | function ($e) use (&$wait) { | |
174 | $wait = false; | |
175 | throw $e; | |
176 | } | |
177 | ); | |
178 | ||
179 | // run loop for short period to ensure we detect connection ICMP rejection error | |
180 | \Clue\React\Block\sleep(0.01, $loop); | |
181 | if ($wait) { | |
182 | \Clue\React\Block\sleep(0.2, $loop); | |
183 | } | |
184 | ||
185 | $this->assertFalse($wait); | |
186 | } | |
187 | ||
188 | public function testQueryResolvesIfServerSendsValidResponse() | |
189 | { | |
190 | $parser = new Parser(); | |
191 | $dumper = new BinaryDumper(); | |
192 | ||
193 | $loop = Factory::create(); | |
194 | ||
195 | $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); | |
196 | $loop->addReadStream($server, function ($server) use ($parser, $dumper) { | |
197 | $data = stream_socket_recvfrom($server, 512, 0, $peer); | |
198 | ||
199 | $message = $parser->parseMessage($data); | |
200 | ||
201 | stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); | |
202 | }); | |
203 | ||
204 | $address = stream_socket_get_name($server, false); | |
205 | $executor = new UdpTransportExecutor($loop, $parser, $dumper); | |
206 | ||
207 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
208 | ||
209 | $promise = $executor->query($address, $query); | |
210 | $response = \Clue\React\Block\await($promise, $loop, 0.2); | |
211 | ||
212 | $this->assertInstanceOf('React\Dns\Model\Message', $response); | |
213 | } | |
214 | } |
11 | 11 | { |
12 | 12 | /** |
13 | 13 | * @covers React\Dns\Resolver\Resolver::resolveAliases |
14 | * @covers React\Dns\Resolver\Resolver::valuesByNameAndType | |
14 | 15 | * @dataProvider provideAliasedAnswers |
15 | 16 | */ |
16 | 17 | public function testResolveAliases(array $expectedAnswers, array $answers, $name) |
7 | 7 | use React\Dns\Model\Record; |
8 | 8 | use React\Promise; |
9 | 9 | use React\Tests\Dns\TestCase; |
10 | use React\Dns\RecordNotFoundException; | |
10 | 11 | |
11 | 12 | class ResolverTest extends TestCase |
12 | 13 | { |
32 | 33 | } |
33 | 34 | |
34 | 35 | /** @test */ |
36 | public function resolveAllShouldQueryGivenRecords() | |
37 | { | |
38 | $executor = $this->createExecutorMock(); | |
39 | $executor | |
40 | ->expects($this->once()) | |
41 | ->method('query') | |
42 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
43 | ->will($this->returnCallback(function ($nameserver, $query) { | |
44 | $response = new Message(); | |
45 | $response->header->set('qr', 1); | |
46 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
47 | $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1'); | |
48 | ||
49 | return Promise\resolve($response); | |
50 | })); | |
51 | ||
52 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
53 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1'))); | |
54 | } | |
55 | ||
56 | /** @test */ | |
57 | public function resolveAllShouldIgnoreRecordsWithOtherTypes() | |
58 | { | |
59 | $executor = $this->createExecutorMock(); | |
60 | $executor | |
61 | ->expects($this->once()) | |
62 | ->method('query') | |
63 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
64 | ->will($this->returnCallback(function ($nameserver, $query) { | |
65 | $response = new Message(); | |
66 | $response->header->set('qr', 1); | |
67 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
68 | $response->answers[] = new Record($query->name, Message::TYPE_TXT, $query->class, 3600, array('ignored')); | |
69 | $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1'); | |
70 | ||
71 | return Promise\resolve($response); | |
72 | })); | |
73 | ||
74 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
75 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1'))); | |
76 | } | |
77 | ||
78 | /** @test */ | |
79 | public function resolveAllShouldReturnMultipleValuesForAlias() | |
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 | $response->answers[] = new Record($query->name, Message::TYPE_CNAME, $query->class, 3600, 'example.com'); | |
91 | $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::1'); | |
92 | $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::2'); | |
93 | $response->prepare(); | |
94 | ||
95 | return Promise\resolve($response); | |
96 | })); | |
97 | ||
98 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
99 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then( | |
100 | $this->expectCallableOnceWith($this->equalTo(array('::1', '::2'))) | |
101 | ); | |
102 | } | |
103 | ||
104 | /** @test */ | |
35 | 105 | public function resolveShouldQueryARecordsAndIgnoreCase() |
36 | 106 | { |
37 | 107 | $executor = $this->createExecutorMock(); |
65 | 135 | $response->header->set('qr', 1); |
66 | 136 | $response->questions[] = new Record($query->name, $query->type, $query->class); |
67 | 137 | $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 | 138 | |
91 | 139 | return Promise\resolve($response); |
92 | 140 | })); |
115 | 163 | return Promise\resolve($response); |
116 | 164 | })); |
117 | 165 | |
118 | $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException')); | |
166 | $errback = $this->expectCallableOnceWith($this->callback(function ($param) { | |
167 | return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io did not return a valid answer (NOERROR / NODATA)'); | |
168 | })); | |
119 | 169 | |
120 | 170 | $resolver = new Resolver('8.8.8.8:53', $executor); |
121 | 171 | $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); |
122 | 172 | } |
123 | 173 | |
174 | public function provideRcodeErrors() | |
175 | { | |
176 | return array( | |
177 | array( | |
178 | Message::RCODE_FORMAT_ERROR, | |
179 | 'DNS query for example.com returned an error response (Format Error)', | |
180 | ), | |
181 | array( | |
182 | Message::RCODE_SERVER_FAILURE, | |
183 | 'DNS query for example.com returned an error response (Server Failure)', | |
184 | ), | |
185 | array( | |
186 | Message::RCODE_NAME_ERROR, | |
187 | 'DNS query for example.com returned an error response (Non-Existent Domain / NXDOMAIN)' | |
188 | ), | |
189 | array( | |
190 | Message::RCODE_NOT_IMPLEMENTED, | |
191 | 'DNS query for example.com returned an error response (Not Implemented)' | |
192 | ), | |
193 | array( | |
194 | Message::RCODE_REFUSED, | |
195 | 'DNS query for example.com returned an error response (Refused)' | |
196 | ), | |
197 | array( | |
198 | 99, | |
199 | 'DNS query for example.com returned an error response (Unknown error response code 99)' | |
200 | ) | |
201 | ); | |
202 | } | |
203 | ||
204 | /** | |
205 | * @test | |
206 | * @dataProvider provideRcodeErrors | |
207 | */ | |
208 | public function resolveWithRcodeErrorShouldCallErrbackIfGiven($code, $expectedMessage) | |
209 | { | |
210 | $executor = $this->createExecutorMock(); | |
211 | $executor | |
212 | ->expects($this->once()) | |
213 | ->method('query') | |
214 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
215 | ->will($this->returnCallback(function ($nameserver, $query) use ($code) { | |
216 | $response = new Message(); | |
217 | $response->header->set('qr', 1); | |
218 | $response->header->set('rcode', $code); | |
219 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
220 | ||
221 | return Promise\resolve($response); | |
222 | })); | |
223 | ||
224 | $errback = $this->expectCallableOnceWith($this->callback(function ($param) use ($code, $expectedMessage) { | |
225 | return ($param instanceof RecordNotFoundException && $param->getCode() === $code && $param->getMessage() === $expectedMessage); | |
226 | })); | |
227 | ||
228 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
229 | $resolver->resolve('example.com')->then($this->expectCallableNever(), $errback); | |
230 | } | |
231 | ||
232 | public function testLegacyExtractAddress() | |
233 | { | |
234 | $executor = $this->createExecutorMock(); | |
235 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
236 | ||
237 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
238 | $response = Message::createResponseWithAnswersForQuery($query, array( | |
239 | new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3600, '1.2.3.4') | |
240 | )); | |
241 | ||
242 | $ret = $resolver->extractAddress($query, $response); | |
243 | $this->assertEquals('1.2.3.4', $ret); | |
244 | } | |
245 | ||
124 | 246 | private function createExecutorMock() |
125 | 247 | { |
126 | 248 | return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); |