New upstream version 1.2.0
Dominik George
3 years ago
7 | 7 | - 7.0 |
8 | 8 | - 7.1 |
9 | 9 | - 7.2 |
10 | - hhvm # ignore errors, see below | |
10 | - 7.3 | |
11 | # - hhvm # requires legacy phpunit & ignore errors, see below | |
11 | 12 | |
12 | 13 | # lock distro so new future defaults will not break the build |
13 | 14 | dist: trusty |
16 | 17 | include: |
17 | 18 | - php: 5.3 |
18 | 19 | dist: precise |
20 | - php: hhvm | |
21 | install: composer require phpunit/phpunit:^5 --dev --no-interaction | |
19 | 22 | allow_failures: |
20 | 23 | - php: hhvm |
21 | 24 |
0 | 0 | # Changelog |
1 | ||
2 | ## 1.2.0 (2019-08-15) | |
3 | ||
4 | * Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection, | |
5 | add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and | |
6 | automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`. | |
7 | (#145, #146, #147 and #148 by @clue) | |
8 | ||
9 | * Feature: Support escaping literal dots and special characters in domain names. | |
10 | (#144 by @clue) | |
11 | ||
12 | ## 1.1.0 (2019-07-18) | |
13 | ||
14 | * Feature: Support parsing `CAA` and `SSHFP` records. | |
15 | (#141 and #142 by @clue) | |
16 | ||
17 | * Feature: Add `ResolverInterface` as common interface for `Resolver` class. | |
18 | (#139 by @clue) | |
19 | ||
20 | * Fix: Add missing private property definitions and | |
21 | remove unneeded dependency on `react/stream`. | |
22 | (#140 and #143 by @clue) | |
23 | ||
24 | ## 1.0.0 (2019-07-11) | |
25 | ||
26 | * First stable LTS release, now following [SemVer](https://semver.org/). | |
27 | We'd like to emphasize that this component is production ready and battle-tested. | |
28 | We plan to support all long-term support (LTS) releases for at least 24 months, | |
29 | so you have a rock-solid foundation to build on top of. | |
30 | ||
31 | This update involves a number of BC breaks due to dropped support for | |
32 | deprecated functionality and some internal API cleanup. We've tried hard to | |
33 | avoid BC breaks where possible and minimize impact otherwise. We expect that | |
34 | most consumers of this package will actually not be affected by any BC | |
35 | breaks, see below for more details: | |
36 | ||
37 | * BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions | |
38 | instead of nested arrays and increase code coverage to 100%. | |
39 | (#130 by @clue) | |
40 | ||
41 | * BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`, | |
42 | remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and | |
43 | add API documentation for `ExecutorInterface`. | |
44 | (#135, #137 and #138 by @clue) | |
45 | ||
46 | * BC break: Replace `HeaderBag` attributes with simple `Message` properties. | |
47 | (#132 by @clue) | |
48 | ||
49 | * BC break: Mark all `Record` attributes as required, add documentation vs `Query`. | |
50 | (#136 by @clue) | |
51 | ||
52 | * BC break: Mark all classes as final to discourage inheritance | |
53 | (#134 by @WyriHaximus) | |
54 | ||
55 | ## 0.4.19 (2019-07-10) | |
56 | ||
57 | * Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6. | |
58 | (#133 by @clue) | |
59 | ||
60 | ## 0.4.18 (2019-09-07) | |
61 | ||
62 | * Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`, | |
63 | respect TTL from response records when caching and do not cache truncated responses. | |
64 | (#129 by @clue) | |
65 | ||
66 | * Feature: Limit cache size to 256 last responses by default. | |
67 | (#127 by @clue) | |
68 | ||
69 | * Feature: Cooperatively resolve hosts to avoid running same query concurrently. | |
70 | (#125 by @clue) | |
71 | ||
72 | ## 0.4.17 (2019-04-01) | |
73 | ||
74 | * Feature: Support parsing `authority` and `additional` records from DNS response. | |
75 | (#123 by @clue) | |
76 | ||
77 | * Feature: Support dumping records as part of outgoing binary DNS message. | |
78 | (#124 by @clue) | |
79 | ||
80 | * Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0 | |
81 | (#121 by @clue) | |
82 | ||
83 | * Improve test suite to add forward compatibility with PHPUnit 7, | |
84 | test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM. | |
85 | (#122 by @clue) | |
1 | 86 | |
2 | 87 | ## 0.4.16 (2018-11-11) |
3 | 88 |
12 | 12 | * [Basic usage](#basic-usage) |
13 | 13 | * [Caching](#caching) |
14 | 14 | * [Custom cache adapter](#custom-cache-adapter) |
15 | * [Resolver](#resolver) | |
15 | * [ResolverInterface](#resolverinterface) | |
16 | 16 | * [resolve()](#resolve) |
17 | 17 | * [resolveAll()](#resolveall) |
18 | 18 | * [Advanced usage](#advanced-usage) |
19 | 19 | * [UdpTransportExecutor](#udptransportexecutor) |
20 | * [TcpTransportExecutor](#tcptransportexecutor) | |
21 | * [SelectiveTransportExecutor](#selectivetransportexecutor) | |
20 | 22 | * [HostsFileExecutor](#hostsfileexecutor) |
21 | 23 | * [Install](#install) |
22 | 24 | * [Tests](#tests) |
110 | 112 | |
111 | 113 | See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). |
112 | 114 | |
113 | ## Resolver | |
115 | ## ResolverInterface | |
116 | ||
117 | <a id="resolver"><!-- legacy reference --></a> | |
114 | 118 | |
115 | 119 | ### resolve() |
116 | 120 | |
207 | 211 | |
208 | 212 | ```php |
209 | 213 | $loop = Factory::create(); |
210 | $executor = new UdpTransportExecutor($loop); | |
214 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
211 | 215 | |
212 | 216 | $executor->query( |
213 | '8.8.8.8:53', | |
214 | 217 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) |
215 | 218 | )->then(function (Message $message) { |
216 | 219 | foreach ($message->answers as $answer) { |
228 | 231 | |
229 | 232 | ```php |
230 | 233 | $executor = new TimeoutExecutor( |
231 | new UdpTransportExecutor($loop), | |
234 | new UdpTransportExecutor($nameserver, $loop), | |
232 | 235 | 3.0, |
233 | 236 | $loop |
234 | 237 | ); |
241 | 244 | ```php |
242 | 245 | $executor = new RetryExecutor( |
243 | 246 | new TimeoutExecutor( |
244 | new UdpTransportExecutor($loop), | |
247 | new UdpTransportExecutor($nameserver, $loop), | |
245 | 248 | 3.0, |
246 | 249 | $loop |
250 | ) | |
251 | ); | |
252 | ``` | |
253 | ||
254 | Note that this executor is entirely async and as such allows you to execute | |
255 | any number of queries concurrently. You should probably limit the number of | |
256 | concurrent queries in your application or you're very likely going to face | |
257 | rate limitations and bans on the resolver end. For many common applications, | |
258 | you may want to avoid sending the same query multiple times when the first | |
259 | one is still pending, so you will likely want to use this in combination with | |
260 | a `CoopExecutor` like this: | |
261 | ||
262 | ```php | |
263 | $executor = new CoopExecutor( | |
264 | new RetryExecutor( | |
265 | new TimeoutExecutor( | |
266 | new UdpTransportExecutor($nameserver, $loop), | |
267 | 3.0, | |
268 | $loop | |
269 | ) | |
247 | 270 | ) |
248 | 271 | ); |
249 | 272 | ``` |
254 | 277 | packages. Higher-level components should take advantage of the Datagram |
255 | 278 | component instead of reimplementing this socket logic from scratch. |
256 | 279 | |
280 | ### TcpTransportExecutor | |
281 | ||
282 | The `TcpTransportExecutor` class can be used to | |
283 | send DNS queries over a TCP/IP stream transport. | |
284 | ||
285 | This is one of the main classes that send a DNS query to your DNS server. | |
286 | ||
287 | For more advanced usages one can utilize this class directly. | |
288 | The following example looks up the `IPv6` address for `reactphp.org`. | |
289 | ||
290 | ```php | |
291 | $loop = Factory::create(); | |
292 | $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
293 | ||
294 | $executor->query( | |
295 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
296 | )->then(function (Message $message) { | |
297 | foreach ($message->answers as $answer) { | |
298 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
299 | } | |
300 | }, 'printf'); | |
301 | ||
302 | $loop->run(); | |
303 | ``` | |
304 | ||
305 | See also [example #92](examples). | |
306 | ||
307 | Note that this executor does not implement a timeout, so you will very likely | |
308 | want to use this in combination with a `TimeoutExecutor` like this: | |
309 | ||
310 | ```php | |
311 | $executor = new TimeoutExecutor( | |
312 | new TcpTransportExecutor($nameserver, $loop), | |
313 | 3.0, | |
314 | $loop | |
315 | ); | |
316 | ``` | |
317 | ||
318 | Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP | |
319 | transport, so you do not necessarily have to implement any retry logic. | |
320 | ||
321 | Note that this executor is entirely async and as such allows you to execute | |
322 | queries concurrently. The first query will establish a TCP/IP socket | |
323 | connection to the DNS server which will be kept open for a short period. | |
324 | Additional queries will automatically reuse this existing socket connection | |
325 | to the DNS server, will pipeline multiple requests over this single | |
326 | connection and will keep an idle connection open for a short period. The | |
327 | initial TCP/IP connection overhead may incur a slight delay if you only send | |
328 | occasional queries – when sending a larger number of concurrent queries over | |
329 | an existing connection, it becomes increasingly more efficient and avoids | |
330 | creating many concurrent sockets like the UDP-based executor. You may still | |
331 | want to limit the number of (concurrent) queries in your application or you | |
332 | may be facing rate limitations and bans on the resolver end. For many common | |
333 | applications, you may want to avoid sending the same query multiple times | |
334 | when the first one is still pending, so you will likely want to use this in | |
335 | combination with a `CoopExecutor` like this: | |
336 | ||
337 | ```php | |
338 | $executor = new CoopExecutor( | |
339 | new TimeoutExecutor( | |
340 | new TcpTransportExecutor($nameserver, $loop), | |
341 | 3.0, | |
342 | $loop | |
343 | ) | |
344 | ); | |
345 | ``` | |
346 | ||
347 | > Internally, this class uses PHP's TCP/IP sockets and does not take advantage | |
348 | of [react/socket](https://github.com/reactphp/socket) purely for | |
349 | organizational reasons to avoid a cyclic dependency between the two | |
350 | packages. Higher-level components should take advantage of the Socket | |
351 | component instead of reimplementing this socket logic from scratch. | |
352 | ||
353 | ### SelectiveTransportExecutor | |
354 | ||
355 | The `SelectiveTransportExecutor` class can be used to | |
356 | Send DNS queries over a UDP or TCP/IP stream transport. | |
357 | ||
358 | This class will automatically choose the correct transport protocol to send | |
359 | a DNS query to your DNS server. It will always try to send it over the more | |
360 | efficient UDP transport first. If this query yields a size related issue | |
361 | (truncated messages), it will retry over a streaming TCP/IP transport. | |
362 | ||
363 | For more advanced usages one can utilize this class directly. | |
364 | The following example looks up the `IPv6` address for `reactphp.org`. | |
365 | ||
366 | ```php | |
367 | $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor); | |
368 | ||
369 | $executor->query( | |
370 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
371 | )->then(function (Message $message) { | |
372 | foreach ($message->answers as $answer) { | |
373 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
374 | } | |
375 | }, 'printf'); | |
376 | ``` | |
377 | ||
378 | Note that this executor only implements the logic to select the correct | |
379 | transport for the given DNS query. Implementing the correct transport logic, | |
380 | implementing timeouts and any retry logic is left up to the given executors, | |
381 | see also [`UdpTransportExecutor`](#udptransportexecutor) and | |
382 | [`TcpTransportExecutor`](#tcptransportexecutor) for more details. | |
383 | ||
384 | Note that this executor is entirely async and as such allows you to execute | |
385 | any number of queries concurrently. You should probably limit the number of | |
386 | concurrent queries in your application or you're very likely going to face | |
387 | rate limitations and bans on the resolver end. For many common applications, | |
388 | you may want to avoid sending the same query multiple times when the first | |
389 | one is still pending, so you will likely want to use this in combination with | |
390 | a `CoopExecutor` like this: | |
391 | ||
392 | ```php | |
393 | $executor = new CoopExecutor( | |
394 | new SelectiveTransportExecutor( | |
395 | $datagramExecutor, | |
396 | $streamExecutor | |
397 | ) | |
398 | ); | |
399 | ``` | |
400 | ||
257 | 401 | ### HostsFileExecutor |
258 | 402 | |
259 | 403 | Note that the above `UdpTransportExecutor` class always performs an actual DNS query. |
263 | 407 | ```php |
264 | 408 | $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); |
265 | 409 | |
266 | $executor = new UdpTransportExecutor($loop); | |
410 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
267 | 411 | $executor = new HostsFileExecutor($hosts, $executor); |
268 | 412 | |
269 | 413 | $executor->query( |
270 | '8.8.8.8:53', | |
271 | 414 | new Query('localhost', Message::TYPE_A, Message::CLASS_IN) |
272 | 415 | ); |
273 | 416 | ``` |
277 | 420 | The recommended way to install this library is [through Composer](https://getcomposer.org). |
278 | 421 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) |
279 | 422 | |
423 | This project follows [SemVer](https://semver.org/). | |
280 | 424 | This will install the latest supported version: |
281 | 425 | |
282 | 426 | ```bash |
283 | $ composer require react/dns:^0.4.16 | |
427 | $ composer require react/dns:^1.2 | |
284 | 428 | ``` |
285 | 429 | |
286 | 430 | 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.5 || ^0.4 || ^0.3", | |
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" | |
7 | "react/cache": "^1.0 || ^0.6 || ^0.5", | |
8 | "react/event-loop": "^1.0 || ^0.5", | |
9 | "react/promise": "^2.7 || ^1.2.1", | |
10 | "react/promise-timer": "^1.2" | |
12 | 11 | }, |
13 | 12 | "require-dev": { |
14 | 13 | "clue/block-react": "^1.2", |
15 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" | |
14 | "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" | |
16 | 15 | }, |
17 | 16 | "autoload": { |
18 | 17 | "psr-4": { "React\\Dns\\": "src" } |
0 | 0 | <?php |
1 | ||
2 | // $ php examples/12-all-types.php | |
3 | // $ php examples/12-all-types.php myserverplace.de SSHFP | |
1 | 4 | |
2 | 5 | use React\Dns\Config\Config; |
3 | 6 | use React\Dns\Resolver\Factory; |
7 | 7 | require __DIR__ . '/../vendor/autoload.php'; |
8 | 8 | |
9 | 9 | $loop = Factory::create(); |
10 | $executor = new UdpTransportExecutor($loop); | |
10 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
11 | 11 | |
12 | 12 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; |
13 | 13 | |
14 | 14 | $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN); |
15 | 15 | $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN); |
16 | 16 | |
17 | $executor->query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) { | |
17 | $executor->query($ipv4Query)->then(function (Message $message) { | |
18 | 18 | foreach ($message->answers as $answer) { |
19 | 19 | echo 'IPv4: ' . $answer->data . PHP_EOL; |
20 | 20 | } |
21 | 21 | }, 'printf'); |
22 | $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) { | |
22 | $executor->query($ipv6Query)->then(function (Message $message) { | |
23 | 23 | foreach ($message->answers as $answer) { |
24 | 24 | echo 'IPv6: ' . $answer->data . PHP_EOL; |
25 | 25 | } |
0 | 0 | <?php |
1 | ||
2 | // $ php examples/92-query-any.php mailbox.org | |
3 | // $ php examples/92-query-any.php _carddav._tcp.mailbox.org | |
1 | 4 | |
2 | 5 | use React\Dns\Model\Message; |
3 | 6 | use React\Dns\Model\Record; |
4 | 7 | use React\Dns\Query\Query; |
5 | use React\Dns\Query\UdpTransportExecutor; | |
8 | use React\Dns\Query\TcpTransportExecutor; | |
6 | 9 | use React\EventLoop\Factory; |
7 | 10 | |
8 | 11 | require __DIR__ . '/../vendor/autoload.php'; |
9 | 12 | |
10 | 13 | $loop = Factory::create(); |
11 | $executor = new UdpTransportExecutor($loop); | |
14 | $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
12 | 15 | |
13 | 16 | $name = isset($argv[1]) ? $argv[1] : 'google.com'; |
14 | 17 | |
15 | 18 | $any = new Query($name, Message::TYPE_ANY, Message::CLASS_IN); |
16 | 19 | |
17 | $executor->query('8.8.8.8:53', $any)->then(function (Message $message) { | |
20 | $executor->query($any)->then(function (Message $message) { | |
18 | 21 | foreach ($message->answers as $answer) { |
19 | 22 | /* @var $answer Record */ |
20 | 23 | |
48 | 51 | $data = implode(' ', $data); |
49 | 52 | break; |
50 | 53 | case Message::TYPE_SRV: |
51 | // SRV records contains priority, weight, port and target, dump structure here | |
54 | // SRV records contain priority, weight, port and target, dump structure here | |
52 | 55 | $type = 'SRV'; |
53 | 56 | $data = json_encode($data); |
57 | break; | |
58 | case Message::TYPE_SSHFP: | |
59 | // SSHFP records contain algorithm, fingerprint type and hex fingerprint value | |
60 | $type = 'SSHFP'; | |
61 | $data = implode(' ', $data); | |
54 | 62 | break; |
55 | 63 | case Message::TYPE_SOA: |
56 | 64 | // SOA records contain structured data, dump structure here |
57 | 65 | $type = 'SOA'; |
58 | 66 | $data = json_encode($data); |
67 | break; | |
68 | case Message::TYPE_CAA: | |
69 | // CAA records contains flag, tag and value | |
70 | $type = 'CAA'; | |
71 | $data = $data['flag'] . ' ' . $data['tag'] . ' "' . $data['value'] . '"'; | |
59 | 72 | break; |
60 | 73 | default: |
61 | 74 | // unknown type uses HEX format |
7 | 7 | convertWarningsToExceptions="true" |
8 | 8 | processIsolation="false" |
9 | 9 | stopOnFailure="false" |
10 | syntaxCheck="false" | |
11 | 10 | bootstrap="vendor/autoload.php" |
12 | 11 | > |
13 | 12 | <testsuites> |
1 | 1 | |
2 | 2 | namespace React\Dns; |
3 | 3 | |
4 | class BadServerException extends \Exception | |
4 | final class BadServerException extends \Exception | |
5 | 5 | { |
6 | 6 | } |
3 | 3 | |
4 | 4 | use RuntimeException; |
5 | 5 | |
6 | class Config | |
6 | final class Config | |
7 | 7 | { |
8 | 8 | /** |
9 | 9 | * Loads the system DNS configuration |
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 | } |
71 | 71 | |
72 | 72 | return new self($contents); |
73 | 73 | } |
74 | ||
75 | private $contents; | |
74 | 76 | |
75 | 77 | /** |
76 | 78 | * Instantiate new hosts file with the given hosts file contents |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Model; | |
3 | ||
4 | class HeaderBag | |
5 | { | |
6 | public $attributes = array( | |
7 | 'qdCount' => 0, | |
8 | 'anCount' => 0, | |
9 | 'nsCount' => 0, | |
10 | 'arCount' => 0, | |
11 | 'qr' => 0, | |
12 | 'opcode' => Message::OPCODE_QUERY, | |
13 | 'aa' => 0, | |
14 | 'tc' => 0, | |
15 | 'rd' => 0, | |
16 | 'ra' => 0, | |
17 | 'z' => 0, | |
18 | 'rcode' => Message::RCODE_OK, | |
19 | ); | |
20 | ||
21 | /** | |
22 | * @deprecated unused, exists for BC only | |
23 | */ | |
24 | public $data = ''; | |
25 | ||
26 | public function get($name) | |
27 | { | |
28 | return isset($this->attributes[$name]) ? $this->attributes[$name] : null; | |
29 | } | |
30 | ||
31 | public function set($name, $value) | |
32 | { | |
33 | $this->attributes[$name] = $value; | |
34 | } | |
35 | ||
36 | public function isQuery() | |
37 | { | |
38 | return 0 === $this->attributes['qr']; | |
39 | } | |
40 | ||
41 | public function isResponse() | |
42 | { | |
43 | return 1 === $this->attributes['qr']; | |
44 | } | |
45 | ||
46 | public function isTruncated() | |
47 | { | |
48 | return 1 === $this->attributes['tc']; | |
49 | } | |
50 | ||
51 | public function populateCounts(Message $message) | |
52 | { | |
53 | $this->attributes['qdCount'] = count($message->questions); | |
54 | $this->attributes['anCount'] = count($message->answers); | |
55 | $this->attributes['nsCount'] = count($message->authority); | |
56 | $this->attributes['arCount'] = count($message->additional); | |
57 | } | |
58 | } |
3 | 3 | |
4 | 4 | use React\Dns\Query\Query; |
5 | 5 | |
6 | class Message | |
6 | /** | |
7 | * This class represents an outgoing query message or an incoming response message | |
8 | * | |
9 | * @link https://tools.ietf.org/html/rfc1035#section-4.1.1 | |
10 | */ | |
11 | final class Message | |
7 | 12 | { |
8 | 13 | const TYPE_A = 1; |
9 | 14 | const TYPE_NS = 2; |
14 | 19 | const TYPE_TXT = 16; |
15 | 20 | const TYPE_AAAA = 28; |
16 | 21 | const TYPE_SRV = 33; |
22 | const TYPE_SSHFP = 44; | |
17 | 23 | const TYPE_ANY = 255; |
24 | const TYPE_CAA = 257; | |
18 | 25 | |
19 | 26 | const CLASS_IN = 1; |
20 | 27 | |
38 | 45 | public static function createRequestForQuery(Query $query) |
39 | 46 | { |
40 | 47 | $request = new Message(); |
41 | $request->header->set('id', self::generateId()); | |
42 | $request->header->set('rd', 1); | |
43 | $request->questions[] = (array) $query; | |
44 | $request->prepare(); | |
48 | $request->id = self::generateId(); | |
49 | $request->rd = true; | |
50 | $request->questions[] = $query; | |
45 | 51 | |
46 | 52 | return $request; |
47 | 53 | } |
56 | 62 | public static function createResponseWithAnswersForQuery(Query $query, array $answers) |
57 | 63 | { |
58 | 64 | $response = new Message(); |
59 | $response->header->set('id', self::generateId()); | |
60 | $response->header->set('qr', 1); | |
61 | $response->header->set('opcode', Message::OPCODE_QUERY); | |
62 | $response->header->set('rd', 1); | |
63 | $response->header->set('rcode', Message::RCODE_OK); | |
65 | $response->id = self::generateId(); | |
66 | $response->qr = true; | |
67 | $response->rd = true; | |
64 | 68 | |
65 | $response->questions[] = (array) $query; | |
69 | $response->questions[] = $query; | |
66 | 70 | |
67 | 71 | foreach ($answers as $record) { |
68 | 72 | $response->answers[] = $record; |
69 | 73 | } |
70 | ||
71 | $response->prepare(); | |
72 | 74 | |
73 | 75 | return $response; |
74 | 76 | } |
99 | 101 | return mt_rand(0, 0xffff); |
100 | 102 | } |
101 | 103 | |
102 | public $header; | |
103 | public $questions = array(); | |
104 | public $answers = array(); | |
105 | public $authority = array(); | |
106 | public $additional = array(); | |
107 | ||
108 | 104 | /** |
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 | */ | |
116 | public $consumed = 0; | |
117 | ||
118 | public function __construct() | |
119 | { | |
120 | $this->header = new HeaderBag(); | |
121 | } | |
122 | ||
123 | /** | |
124 | * Returns the 16 bit message ID | |
105 | * The 16 bit message ID | |
125 | 106 | * |
126 | 107 | * The response message ID has to match the request message ID. This allows |
127 | 108 | * the receiver to verify this is the correct response message. An outside |
128 | 109 | * attacker may try to inject fake responses by "guessing" the message ID, |
129 | 110 | * so this should use a proper CSPRNG to avoid possible cache poisoning. |
130 | 111 | * |
131 | * @return int | |
112 | * @var int 16 bit message ID | |
132 | 113 | * @see self::generateId() |
133 | 114 | */ |
134 | public function getId() | |
135 | { | |
136 | return $this->header->get('id'); | |
137 | } | |
115 | public $id = 0; | |
138 | 116 | |
139 | 117 | /** |
140 | * Returns the response code (RCODE) | |
118 | * @var bool Query/Response flag, query=false or response=true | |
119 | */ | |
120 | public $qr = false; | |
121 | ||
122 | /** | |
123 | * @var int specifies the kind of query (4 bit), see self::OPCODE_* constants | |
124 | * @see self::OPCODE_QUERY | |
125 | */ | |
126 | public $opcode = self::OPCODE_QUERY; | |
127 | ||
128 | /** | |
141 | 129 | * |
142 | * @return int see self::RCODE_* constants | |
130 | * @var bool Authoritative Answer | |
143 | 131 | */ |
144 | public function getResponseCode() | |
145 | { | |
146 | return $this->header->get('rcode'); | |
147 | } | |
132 | public $aa = false; | |
148 | 133 | |
149 | public function prepare() | |
150 | { | |
151 | $this->header->populateCounts($this); | |
152 | } | |
134 | /** | |
135 | * @var bool TrunCation | |
136 | */ | |
137 | public $tc = false; | |
138 | ||
139 | /** | |
140 | * @var bool Recursion Desired | |
141 | */ | |
142 | public $rd = false; | |
143 | ||
144 | /** | |
145 | * @var bool Recursion Available | |
146 | */ | |
147 | public $ra = false; | |
148 | ||
149 | /** | |
150 | * @var int response code (4 bit), see self::RCODE_* constants | |
151 | * @see self::RCODE_OK | |
152 | */ | |
153 | public $rcode = Message::RCODE_OK; | |
154 | ||
155 | /** | |
156 | * An array of Query objects | |
157 | * | |
158 | * ```php | |
159 | * $questions = array( | |
160 | * new Query( | |
161 | * 'reactphp.org', | |
162 | * Message::TYPE_A, | |
163 | * Message::CLASS_IN | |
164 | * ) | |
165 | * ); | |
166 | * ``` | |
167 | * | |
168 | * @var Query[] | |
169 | */ | |
170 | public $questions = array(); | |
171 | ||
172 | /** | |
173 | * @var Record[] | |
174 | */ | |
175 | public $answers = array(); | |
176 | ||
177 | /** | |
178 | * @var Record[] | |
179 | */ | |
180 | public $authority = array(); | |
181 | ||
182 | /** | |
183 | * @var Record[] | |
184 | */ | |
185 | public $additional = array(); | |
153 | 186 | } |
1 | 1 | |
2 | 2 | namespace React\Dns\Model; |
3 | 3 | |
4 | class Record | |
4 | /** | |
5 | * This class represents a single resulting record in a response message | |
6 | * | |
7 | * It uses a structure similar to `\React\Dns\Query\Query`, but does include | |
8 | * fields for resulting TTL and resulting record data (IPs etc.). | |
9 | * | |
10 | * @link https://tools.ietf.org/html/rfc1035#section-4.1.3 | |
11 | * @see \React\Dns\Query\Query | |
12 | */ | |
13 | final class Record | |
5 | 14 | { |
6 | 15 | /** |
7 | 16 | * @var string hostname without trailing dot, for example "reactphp.org" |
33 | 42 | * |
34 | 43 | * - A: |
35 | 44 | * IPv4 address string, for example "192.168.1.1". |
45 | * | |
36 | 46 | * - AAAA: |
37 | 47 | * IPv6 address string, for example "::1". |
48 | * | |
38 | 49 | * - CNAME / PTR / NS: |
39 | 50 | * The hostname without trailing dot, for example "reactphp.org". |
51 | * | |
40 | 52 | * - TXT: |
41 | 53 | * List of string values, for example `["v=spf1 include:example.com"]`. |
42 | 54 | * This is commonly a list with only a single string value, but this |
48 | 60 | * suggests using key-value pairs such as `["name=test","version=1"]`, but |
49 | 61 | * interpretation of this is not enforced and left up to consumers of this |
50 | 62 | * library (used for DNS-SD/Zeroconf and others). |
63 | * | |
51 | 64 | * - MX: |
52 | 65 | * Mail server priority (UINT16) and target hostname without trailing dot, |
53 | 66 | * for example `{"priority":10,"target":"mx.example.com"}`. |
56 | 69 | * referred to as exchange). If a response message contains multiple |
57 | 70 | * records of this type, targets should be sorted by priority (lowest |
58 | 71 | * first) - this is left up to consumers of this library (used for SMTP). |
72 | * | |
59 | 73 | * - SRV: |
60 | 74 | * Service priority (UINT16), service weight (UINT16), service port (UINT16) |
61 | 75 | * and target hostname without trailing dot, for example |
68 | 82 | * randomly according to their weight - this is left up to consumers of |
69 | 83 | * this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782) |
70 | 84 | * for more details. |
85 | * | |
86 | * - SSHFP: | |
87 | * Includes algorithm (UNIT8), fingerprint type (UNIT8) and fingerprint | |
88 | * value as lower case hex string, for example: | |
89 | * `{"algorithm":1,"type":1,"fingerprint":"0123456789abcdef..."}` | |
90 | * See also https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml | |
91 | * for algorithm and fingerprint type assignments. | |
92 | * | |
71 | 93 | * - SOA: |
72 | 94 | * Includes master hostname without trailing dot, responsible person email |
73 | 95 | * as hostname without trailing dot and serial, refresh, retry, expire and |
74 | 96 | * minimum times in seconds (UINT32 each), for example: |
75 | 97 | * `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial": |
76 | 98 | * 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`. |
99 | * | |
100 | * - CAA: | |
101 | * Includes flag (UNIT8), tag string and value string, for example: | |
102 | * `{"flag":128,"tag":"issue","value":"letsencrypt.org"}` | |
103 | * | |
77 | 104 | * - Any other unknown type: |
78 | 105 | * An opaque binary string containing the RDATA as transported in the DNS |
79 | 106 | * record. For forwards compatibility, you should not rely on this format |
86 | 113 | */ |
87 | 114 | public $data; |
88 | 115 | |
89 | public function __construct($name, $type, $class, $ttl = 0, $data = null) | |
116 | /** | |
117 | * @param string $name | |
118 | * @param int $type | |
119 | * @param int $class | |
120 | * @param int $ttl | |
121 | * @param string|string[]|array $data | |
122 | */ | |
123 | public function __construct($name, $type, $class, $ttl, $data) | |
90 | 124 | { |
91 | 125 | $this->name = $name; |
92 | 126 | $this->type = $type; |
2 | 2 | namespace React\Dns\Protocol; |
3 | 3 | |
4 | 4 | use React\Dns\Model\Message; |
5 | use React\Dns\Model\HeaderBag; | |
5 | use React\Dns\Model\Record; | |
6 | use React\Dns\Query\Query; | |
6 | 7 | |
7 | class BinaryDumper | |
8 | final class BinaryDumper | |
8 | 9 | { |
10 | /** | |
11 | * @param Message $message | |
12 | * @return string | |
13 | */ | |
9 | 14 | public function toBinary(Message $message) |
10 | 15 | { |
11 | 16 | $data = ''; |
12 | 17 | |
13 | $data .= $this->headerToBinary($message->header); | |
18 | $data .= $this->headerToBinary($message); | |
14 | 19 | $data .= $this->questionToBinary($message->questions); |
20 | $data .= $this->recordsToBinary($message->answers); | |
21 | $data .= $this->recordsToBinary($message->authority); | |
22 | $data .= $this->recordsToBinary($message->additional); | |
15 | 23 | |
16 | 24 | return $data; |
17 | 25 | } |
18 | 26 | |
19 | private function headerToBinary(HeaderBag $header) | |
27 | /** | |
28 | * @param Message $message | |
29 | * @return string | |
30 | */ | |
31 | private function headerToBinary(Message $message) | |
20 | 32 | { |
21 | 33 | $data = ''; |
22 | 34 | |
23 | $data .= pack('n', $header->get('id')); | |
35 | $data .= pack('n', $message->id); | |
24 | 36 | |
25 | 37 | $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'); | |
38 | $flags = ($flags << 1) | ($message->qr ? 1 : 0); | |
39 | $flags = ($flags << 4) | $message->opcode; | |
40 | $flags = ($flags << 1) | ($message->aa ? 1 : 0); | |
41 | $flags = ($flags << 1) | ($message->tc ? 1 : 0); | |
42 | $flags = ($flags << 1) | ($message->rd ? 1 : 0); | |
43 | $flags = ($flags << 1) | ($message->ra ? 1 : 0); | |
44 | $flags = ($flags << 3) | 0; // skip unused zero bit | |
45 | $flags = ($flags << 4) | $message->rcode; | |
34 | 46 | |
35 | 47 | $data .= pack('n', $flags); |
36 | 48 | |
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')); | |
49 | $data .= pack('n', count($message->questions)); | |
50 | $data .= pack('n', count($message->answers)); | |
51 | $data .= pack('n', count($message->authority)); | |
52 | $data .= pack('n', count($message->additional)); | |
41 | 53 | |
42 | 54 | return $data; |
43 | 55 | } |
44 | 56 | |
57 | /** | |
58 | * @param Query[] $questions | |
59 | * @return string | |
60 | */ | |
45 | 61 | private function questionToBinary(array $questions) |
46 | 62 | { |
47 | 63 | $data = ''; |
48 | 64 | |
49 | 65 | 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']); | |
66 | $data .= $this->domainNameToBinary($question->name); | |
67 | $data .= pack('n*', $question->type, $question->class); | |
57 | 68 | } |
58 | 69 | |
59 | 70 | return $data; |
60 | 71 | } |
72 | ||
73 | /** | |
74 | * @param Record[] $records | |
75 | * @return string | |
76 | */ | |
77 | private function recordsToBinary(array $records) | |
78 | { | |
79 | $data = ''; | |
80 | ||
81 | foreach ($records as $record) { | |
82 | /* @var $record Record */ | |
83 | switch ($record->type) { | |
84 | case Message::TYPE_A: | |
85 | case Message::TYPE_AAAA: | |
86 | $binary = \inet_pton($record->data); | |
87 | break; | |
88 | case Message::TYPE_CNAME: | |
89 | case Message::TYPE_NS: | |
90 | case Message::TYPE_PTR: | |
91 | $binary = $this->domainNameToBinary($record->data); | |
92 | break; | |
93 | case Message::TYPE_TXT: | |
94 | $binary = $this->textsToBinary($record->data); | |
95 | break; | |
96 | case Message::TYPE_MX: | |
97 | $binary = \pack( | |
98 | 'n', | |
99 | $record->data['priority'] | |
100 | ); | |
101 | $binary .= $this->domainNameToBinary($record->data['target']); | |
102 | break; | |
103 | case Message::TYPE_SRV: | |
104 | $binary = \pack( | |
105 | 'n*', | |
106 | $record->data['priority'], | |
107 | $record->data['weight'], | |
108 | $record->data['port'] | |
109 | ); | |
110 | $binary .= $this->domainNameToBinary($record->data['target']); | |
111 | break; | |
112 | case Message::TYPE_SOA: | |
113 | $binary = $this->domainNameToBinary($record->data['mname']); | |
114 | $binary .= $this->domainNameToBinary($record->data['rname']); | |
115 | $binary .= \pack( | |
116 | 'N*', | |
117 | $record->data['serial'], | |
118 | $record->data['refresh'], | |
119 | $record->data['retry'], | |
120 | $record->data['expire'], | |
121 | $record->data['minimum'] | |
122 | ); | |
123 | break; | |
124 | case Message::TYPE_CAA: | |
125 | $binary = \pack( | |
126 | 'C*', | |
127 | $record->data['flag'], | |
128 | \strlen($record->data['tag']) | |
129 | ); | |
130 | $binary .= $record->data['tag']; | |
131 | $binary .= $record->data['value']; | |
132 | break; | |
133 | case Message::TYPE_SSHFP: | |
134 | $binary = \pack( | |
135 | 'CCH*', | |
136 | $record->data['algorithm'], | |
137 | $record->data['type'], | |
138 | $record->data['fingerprint'] | |
139 | ); | |
140 | break; | |
141 | default: | |
142 | // RDATA is already stored as binary value for unknown record types | |
143 | $binary = $record->data; | |
144 | } | |
145 | ||
146 | $data .= $this->domainNameToBinary($record->name); | |
147 | $data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary)); | |
148 | $data .= $binary; | |
149 | } | |
150 | ||
151 | return $data; | |
152 | } | |
153 | ||
154 | /** | |
155 | * @param string[] $texts | |
156 | * @return string | |
157 | */ | |
158 | private function textsToBinary(array $texts) | |
159 | { | |
160 | $data = ''; | |
161 | foreach ($texts as $text) { | |
162 | $data .= \chr(\strlen($text)) . $text; | |
163 | } | |
164 | return $data; | |
165 | } | |
166 | ||
167 | /** | |
168 | * @param string $host | |
169 | * @return string | |
170 | */ | |
171 | private function domainNameToBinary($host) | |
172 | { | |
173 | if ($host === '') { | |
174 | return "\0"; | |
175 | } | |
176 | ||
177 | // break up domain name at each dot that is not preceeded by a backslash (escaped notation) | |
178 | return $this->textsToBinary( | |
179 | \array_map( | |
180 | 'stripcslashes', | |
181 | \preg_split( | |
182 | '/(?<!\\\\)\./', | |
183 | $host . '.' | |
184 | ) | |
185 | ) | |
186 | ); | |
187 | } | |
61 | 188 | } |
3 | 3 | |
4 | 4 | use React\Dns\Model\Message; |
5 | 5 | use React\Dns\Model\Record; |
6 | use React\Dns\Query\Query; | |
6 | 7 | use InvalidArgumentException; |
7 | 8 | |
8 | 9 | /** |
10 | 11 | * |
11 | 12 | * Obsolete and uncommon types and classes are not implemented. |
12 | 13 | */ |
13 | class Parser | |
14 | final class Parser | |
14 | 15 | { |
15 | 16 | /** |
16 | 17 | * Parses the given raw binary message into a Message object |
21 | 22 | */ |
22 | 23 | public function parseMessage($data) |
23 | 24 | { |
25 | // create empty message with two additional, temporary properties for parser | |
24 | 26 | $message = new Message(); |
27 | $message->data = $data; | |
28 | $message->consumed = null; | |
29 | ||
25 | 30 | if ($this->parse($data, $message) !== $message) { |
26 | 31 | throw new InvalidArgumentException('Unable to parse binary message'); |
27 | 32 | } |
28 | 33 | |
34 | unset($message->data, $message->consumed); | |
35 | ||
29 | 36 | return $message; |
30 | 37 | } |
31 | 38 | |
32 | /** | |
33 | * @deprecated unused, exists for BC only | |
34 | * @codeCoverageIgnore | |
35 | */ | |
36 | public function parseChunk($data, Message $message) | |
37 | { | |
38 | return $this->parse($data, $message); | |
39 | } | |
40 | ||
41 | 39 | private function parse($data, Message $message) |
42 | { | |
43 | $message->data .= $data; | |
44 | ||
45 | if (!$message->header->get('id')) { | |
46 | if (!$this->parseHeader($message)) { | |
47 | return; | |
48 | } | |
49 | } | |
50 | ||
51 | if ($message->header->get('qdCount') != count($message->questions)) { | |
52 | if (!$this->parseQuestion($message)) { | |
53 | return; | |
54 | } | |
55 | } | |
56 | ||
57 | if ($message->header->get('anCount') != count($message->answers)) { | |
58 | if (!$this->parseAnswer($message)) { | |
59 | return; | |
60 | } | |
61 | } | |
62 | ||
63 | return $message; | |
64 | } | |
65 | ||
66 | public function parseHeader(Message $message) | |
67 | 40 | { |
68 | 41 | if (!isset($message->data[12 - 1])) { |
69 | 42 | return; |
70 | 43 | } |
71 | 44 | |
72 | $header = substr($message->data, 0, 12); | |
45 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($message->data, 0, 12))); | |
73 | 46 | $message->consumed += 12; |
74 | 47 | |
75 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); | |
76 | ||
77 | $rcode = $fields & bindec('1111'); | |
78 | $z = ($fields >> 4) & bindec('111'); | |
79 | $ra = ($fields >> 7) & 1; | |
80 | $rd = ($fields >> 8) & 1; | |
81 | $tc = ($fields >> 9) & 1; | |
82 | $aa = ($fields >> 10) & 1; | |
83 | $opcode = ($fields >> 11) & bindec('1111'); | |
84 | $qr = ($fields >> 15) & 1; | |
85 | ||
86 | $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', | |
87 | 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); | |
88 | ||
89 | ||
90 | foreach ($vars as $name => $value) { | |
91 | $message->header->set($name, $value); | |
48 | $message->id = $id; | |
49 | $message->rcode = $fields & 0xf; | |
50 | $message->ra = (($fields >> 7) & 1) === 1; | |
51 | $message->rd = (($fields >> 8) & 1) === 1; | |
52 | $message->tc = (($fields >> 9) & 1) === 1; | |
53 | $message->aa = (($fields >> 10) & 1) === 1; | |
54 | $message->opcode = ($fields >> 11) & 0xf; | |
55 | $message->qr = (($fields >> 15) & 1) === 1; | |
56 | ||
57 | // parse all questions | |
58 | for ($i = $qdCount; $i > 0; --$i) { | |
59 | $question = $this->parseQuestion($message); | |
60 | if ($question === null) { | |
61 | return; | |
62 | } else { | |
63 | $message->questions[] = $question; | |
64 | } | |
65 | } | |
66 | ||
67 | // parse all answer records | |
68 | for ($i = $anCount; $i > 0; --$i) { | |
69 | $record = $this->parseRecord($message); | |
70 | if ($record === null) { | |
71 | return; | |
72 | } else { | |
73 | $message->answers[] = $record; | |
74 | } | |
75 | } | |
76 | ||
77 | // parse all authority records | |
78 | for ($i = $nsCount; $i > 0; --$i) { | |
79 | $record = $this->parseRecord($message); | |
80 | if ($record === null) { | |
81 | return; | |
82 | } else { | |
83 | $message->authority[] = $record; | |
84 | } | |
85 | } | |
86 | ||
87 | // parse all additional records | |
88 | for ($i = $arCount; $i > 0; --$i) { | |
89 | $record = $this->parseRecord($message); | |
90 | if ($record === null) { | |
91 | return; | |
92 | } else { | |
93 | $message->additional[] = $record; | |
94 | } | |
92 | 95 | } |
93 | 96 | |
94 | 97 | return $message; |
95 | 98 | } |
96 | 99 | |
97 | public function parseQuestion(Message $message) | |
100 | /** | |
101 | * @param Message $message | |
102 | * @return ?Query | |
103 | */ | |
104 | private function parseQuestion(Message $message) | |
98 | 105 | { |
99 | 106 | $consumed = $message->consumed; |
100 | 107 | |
109 | 116 | |
110 | 117 | $message->consumed = $consumed; |
111 | 118 | |
112 | $message->questions[] = array( | |
113 | 'name' => implode('.', $labels), | |
114 | 'type' => $type, | |
115 | 'class' => $class, | |
119 | return new Query( | |
120 | implode('.', $labels), | |
121 | $type, | |
122 | $class | |
116 | 123 | ); |
117 | ||
118 | if ($message->header->get('qdCount') != count($message->questions)) { | |
119 | return $this->parseQuestion($message); | |
120 | } | |
121 | ||
122 | return $message; | |
123 | } | |
124 | ||
125 | public function parseAnswer(Message $message) | |
124 | } | |
125 | ||
126 | /** | |
127 | * @param Message $message | |
128 | * @return ?Record returns parsed Record on success or null if data is invalid/incomplete | |
129 | */ | |
130 | private function parseRecord(Message $message) | |
126 | 131 | { |
127 | 132 | $consumed = $message->consumed; |
128 | 133 | |
129 | 134 | list($name, $consumed) = $this->readDomain($message->data, $consumed); |
130 | 135 | |
131 | 136 | if ($name === null || !isset($message->data[$consumed + 10 - 1])) { |
132 | return; | |
137 | return null; | |
133 | 138 | } |
134 | 139 | |
135 | 140 | list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); |
147 | 152 | $consumed += 2; |
148 | 153 | |
149 | 154 | if (!isset($message->data[$consumed + $rdLength - 1])) { |
150 | return; | |
155 | return null; | |
151 | 156 | } |
152 | 157 | |
153 | 158 | $rdata = null; |
192 | 197 | 'weight' => $weight, |
193 | 198 | 'port' => $port, |
194 | 199 | 'target' => $target |
200 | ); | |
201 | } | |
202 | } elseif (Message::TYPE_SSHFP === $type) { | |
203 | if ($rdLength > 2) { | |
204 | list($algorithm, $hash) = \array_values(\unpack('C*', \substr($message->data, $consumed, 2))); | |
205 | $fingerprint = \bin2hex(\substr($message->data, $consumed + 2, $rdLength - 2)); | |
206 | $consumed += $rdLength; | |
207 | ||
208 | $rdata = array( | |
209 | 'algorithm' => $algorithm, | |
210 | 'type' => $hash, | |
211 | 'fingerprint' => $fingerprint | |
195 | 212 | ); |
196 | 213 | } |
197 | 214 | } elseif (Message::TYPE_SOA === $type) { |
212 | 229 | 'minimum' => $minimum |
213 | 230 | ); |
214 | 231 | } |
232 | } elseif (Message::TYPE_CAA === $type) { | |
233 | if ($rdLength > 3) { | |
234 | list($flag, $tagLength) = array_values(unpack('C*', substr($message->data, $consumed, 2))); | |
235 | ||
236 | if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) { | |
237 | $tag = substr($message->data, $consumed + 2, $tagLength); | |
238 | $value = substr($message->data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength); | |
239 | $consumed += $rdLength; | |
240 | ||
241 | $rdata = array( | |
242 | 'flag' => $flag, | |
243 | 'tag' => $tag, | |
244 | 'value' => $value | |
245 | ); | |
246 | } | |
247 | } | |
215 | 248 | } else { |
216 | 249 | // unknown types simply parse rdata as an opaque binary string |
217 | 250 | $rdata = substr($message->data, $consumed, $rdLength); |
220 | 253 | |
221 | 254 | // ensure parsing record data consumes expact number of bytes indicated in record length |
222 | 255 | if ($consumed !== $expected || $rdata === null) { |
223 | return; | |
256 | return null; | |
224 | 257 | } |
225 | 258 | |
226 | 259 | $message->consumed = $consumed; |
227 | 260 | |
228 | $record = new Record($name, $type, $class, $ttl, $rdata); | |
229 | ||
230 | $message->answers[] = $record; | |
231 | ||
232 | if ($message->header->get('anCount') != count($message->answers)) { | |
233 | return $this->parseAnswer($message); | |
234 | } | |
235 | ||
236 | return $message; | |
261 | return new Record($name, $type, $class, $ttl, $rdata); | |
237 | 262 | } |
238 | 263 | |
239 | 264 | private function readDomain($data, $consumed) |
244 | 269 | return array(null, null); |
245 | 270 | } |
246 | 271 | |
247 | return array(implode('.', $labels), $consumed); | |
272 | // use escaped notation for each label part, then join using dots | |
273 | return array( | |
274 | \implode( | |
275 | '.', | |
276 | \array_map( | |
277 | function ($label) { | |
278 | return \addcslashes($label, "\0..\40.\177"); | |
279 | }, | |
280 | $labels | |
281 | ) | |
282 | ), | |
283 | $consumed | |
284 | ); | |
248 | 285 | } |
249 | 286 | |
250 | 287 | private function readLabels($data, $consumed) |
293 | 330 | |
294 | 331 | return array($labels, $consumed); |
295 | 332 | } |
296 | ||
297 | /** | |
298 | * @deprecated unused, exists for BC only | |
299 | * @codeCoverageIgnore | |
300 | */ | |
301 | public function isEndOfLabels($data, $consumed) | |
302 | { | |
303 | $length = ord(substr($data, $consumed, 1)); | |
304 | return 0 === $length; | |
305 | } | |
306 | ||
307 | /** | |
308 | * @deprecated unused, exists for BC only | |
309 | * @codeCoverageIgnore | |
310 | */ | |
311 | public function getCompressedLabel($data, $consumed) | |
312 | { | |
313 | list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); | |
314 | list($labels) = $this->readLabels($data, $nameOffset); | |
315 | ||
316 | return array($labels, $consumed); | |
317 | } | |
318 | ||
319 | /** | |
320 | * @deprecated unused, exists for BC only | |
321 | * @codeCoverageIgnore | |
322 | */ | |
323 | public function isCompressedLabel($data, $consumed) | |
324 | { | |
325 | $mask = 0xc000; // 1100000000000000 | |
326 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); | |
327 | ||
328 | return (bool) ($peek & $mask); | |
329 | } | |
330 | ||
331 | /** | |
332 | * @deprecated unused, exists for BC only | |
333 | * @codeCoverageIgnore | |
334 | */ | |
335 | public function getCompressedLabelOffset($data, $consumed) | |
336 | { | |
337 | $mask = 0x3fff; // 0011111111111111 | |
338 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); | |
339 | ||
340 | return array($peek & $mask, $consumed + 2); | |
341 | } | |
342 | ||
343 | /** | |
344 | * @deprecated unused, exists for BC only | |
345 | * @codeCoverageIgnore | |
346 | */ | |
347 | public function signedLongToUnsignedLong($i) | |
348 | { | |
349 | return $i & 0x80000000 ? $i - 0xffffffff : $i; | |
350 | } | |
351 | 333 | } |
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 | use React\Cache\CacheInterface; | |
5 | use React\Dns\Model\Message; | |
6 | use React\Promise\Promise; | |
7 | ||
8 | final class CachingExecutor implements ExecutorInterface | |
9 | { | |
10 | /** | |
11 | * Default TTL for negative responses (NXDOMAIN etc.). | |
12 | * | |
13 | * @internal | |
14 | */ | |
15 | const TTL = 60; | |
16 | ||
17 | private $executor; | |
18 | private $cache; | |
19 | ||
20 | public function __construct(ExecutorInterface $executor, CacheInterface $cache) | |
21 | { | |
22 | $this->executor = $executor; | |
23 | $this->cache = $cache; | |
24 | } | |
25 | ||
26 | public function query(Query $query) | |
27 | { | |
28 | $id = $query->name . ':' . $query->type . ':' . $query->class; | |
29 | $cache = $this->cache; | |
30 | $that = $this; | |
31 | $executor = $this->executor; | |
32 | ||
33 | $pending = $cache->get($id); | |
34 | return new Promise(function ($resolve, $reject) use ($query, $id, $cache, $executor, &$pending, $that) { | |
35 | $pending->then( | |
36 | function ($message) use ($query, $id, $cache, $executor, &$pending, $that) { | |
37 | // return cached response message on cache hit | |
38 | if ($message !== null) { | |
39 | return $message; | |
40 | } | |
41 | ||
42 | // perform DNS lookup if not already cached | |
43 | return $pending = $executor->query($query)->then( | |
44 | function (Message $message) use ($cache, $id, $that) { | |
45 | // DNS response message received => store in cache when not truncated and return | |
46 | if (!$message->tc) { | |
47 | $cache->set($id, $message, $that->ttl($message)); | |
48 | } | |
49 | ||
50 | return $message; | |
51 | } | |
52 | ); | |
53 | } | |
54 | )->then($resolve, function ($e) use ($reject, &$pending) { | |
55 | $reject($e); | |
56 | $pending = null; | |
57 | }); | |
58 | }, function ($_, $reject) use (&$pending, $query) { | |
59 | $reject(new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled')); | |
60 | $pending->cancel(); | |
61 | $pending = null; | |
62 | }); | |
63 | } | |
64 | ||
65 | /** | |
66 | * @param Message $message | |
67 | * @return int | |
68 | * @internal | |
69 | */ | |
70 | public function ttl(Message $message) | |
71 | { | |
72 | // select TTL from answers (should all be the same), use smallest value if available | |
73 | // @link https://tools.ietf.org/html/rfc2181#section-5.2 | |
74 | $ttl = null; | |
75 | foreach ($message->answers as $answer) { | |
76 | if ($ttl === null || $answer->ttl < $ttl) { | |
77 | $ttl = $answer->ttl; | |
78 | } | |
79 | } | |
80 | ||
81 | if ($ttl === null) { | |
82 | $ttl = self::TTL; | |
83 | } | |
84 | ||
85 | return $ttl; | |
86 | } | |
87 | } |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | class CancellationException extends \RuntimeException | |
4 | final class CancellationException extends \RuntimeException | |
5 | 5 | { |
6 | 6 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Promise\Promise; | |
5 | ||
6 | /** | |
7 | * Cooperatively resolves hosts via the given base executor to ensure same query is not run concurrently | |
8 | * | |
9 | * Wraps an existing `ExecutorInterface` to keep tracking of pending queries | |
10 | * and only starts a new query when the same query is not already pending. Once | |
11 | * the underlying query is fulfilled/rejected, it will forward its value to all | |
12 | * promises awaiting the same query. | |
13 | * | |
14 | * This means it will not limit concurrency for queries that differ, for example | |
15 | * when sending many queries for different host names or types. | |
16 | * | |
17 | * This is useful because all executors are entirely async and as such allow you | |
18 | * to execute any number of queries concurrently. You should probably limit the | |
19 | * number of concurrent queries in your application or you're very likely going | |
20 | * to face rate limitations and bans on the resolver end. For many common | |
21 | * applications, you may want to avoid sending the same query multiple times | |
22 | * when the first one is still pending, so you will likely want to use this in | |
23 | * combination with some other executor like this: | |
24 | * | |
25 | * ```php | |
26 | * $executor = new CoopExecutor( | |
27 | * new RetryExecutor( | |
28 | * new TimeoutExecutor( | |
29 | * new UdpTransportExecutor($nameserver, $loop), | |
30 | * 3.0, | |
31 | * $loop | |
32 | * ) | |
33 | * ) | |
34 | * ); | |
35 | * ``` | |
36 | */ | |
37 | final class CoopExecutor implements ExecutorInterface | |
38 | { | |
39 | private $executor; | |
40 | private $pending = array(); | |
41 | private $counts = array(); | |
42 | ||
43 | public function __construct(ExecutorInterface $base) | |
44 | { | |
45 | $this->executor = $base; | |
46 | } | |
47 | ||
48 | public function query(Query $query) | |
49 | { | |
50 | $key = $this->serializeQueryToIdentity($query); | |
51 | if (isset($this->pending[$key])) { | |
52 | // same query is already pending, so use shared reference to pending query | |
53 | $promise = $this->pending[$key]; | |
54 | ++$this->counts[$key]; | |
55 | } else { | |
56 | // no such query pending, so start new query and keep reference until it's fulfilled or rejected | |
57 | $promise = $this->executor->query($query); | |
58 | $this->pending[$key] = $promise; | |
59 | $this->counts[$key] = 1; | |
60 | ||
61 | $pending =& $this->pending; | |
62 | $counts =& $this->counts; | |
63 | $promise->then(function () use ($key, &$pending, &$counts) { | |
64 | unset($pending[$key], $counts[$key]); | |
65 | }, function () use ($key, &$pending, &$counts) { | |
66 | unset($pending[$key], $counts[$key]); | |
67 | }); | |
68 | } | |
69 | ||
70 | // Return a child promise awaiting the pending query. | |
71 | // Cancelling this child promise should only cancel the pending query | |
72 | // when no other child promise is awaiting the same query. | |
73 | $pending =& $this->pending; | |
74 | $counts =& $this->counts; | |
75 | return new Promise(function ($resolve, $reject) use ($promise) { | |
76 | $promise->then($resolve, $reject); | |
77 | }, function () use (&$promise, $key, $query, &$pending, &$counts) { | |
78 | if (--$counts[$key] < 1) { | |
79 | unset($pending[$key], $counts[$key]); | |
80 | $promise->cancel(); | |
81 | $promise = null; | |
82 | } | |
83 | throw new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled'); | |
84 | }); | |
85 | } | |
86 | ||
87 | private function serializeQueryToIdentity(Query $query) | |
88 | { | |
89 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); | |
90 | } | |
91 | } |
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 | /** | |
14 | * @deprecated unused, exists for BC only | |
15 | * @see UdpTransportExecutor | |
16 | */ | |
17 | class Executor implements ExecutorInterface | |
18 | { | |
19 | private $loop; | |
20 | private $parser; | |
21 | private $dumper; | |
22 | private $timeout; | |
23 | ||
24 | /** | |
25 | * | |
26 | * Note that albeit supported, the $timeout parameter is deprecated! | |
27 | * You should pass a `null` value here instead. If you need timeout handling, | |
28 | * use the `TimeoutConnector` instead. | |
29 | * | |
30 | * @param LoopInterface $loop | |
31 | * @param Parser $parser | |
32 | * @param BinaryDumper $dumper | |
33 | * @param null|float $timeout DEPRECATED: timeout for DNS query or NULL=no timeout | |
34 | */ | |
35 | public function __construct(LoopInterface $loop, Parser $parser, BinaryDumper $dumper, $timeout = 5) | |
36 | { | |
37 | $this->loop = $loop; | |
38 | $this->parser = $parser; | |
39 | $this->dumper = $dumper; | |
40 | $this->timeout = $timeout; | |
41 | } | |
42 | ||
43 | public function query($nameserver, Query $query) | |
44 | { | |
45 | $request = Message::createRequestForQuery($query); | |
46 | ||
47 | $queryData = $this->dumper->toBinary($request); | |
48 | $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; | |
49 | ||
50 | return $this->doQuery($nameserver, $transport, $queryData, $query->name); | |
51 | } | |
52 | ||
53 | /** | |
54 | * @deprecated unused, exists for BC only | |
55 | */ | |
56 | public function prepareRequest(Query $query) | |
57 | { | |
58 | return Message::createRequestForQuery($query); | |
59 | } | |
60 | ||
61 | public function doQuery($nameserver, $transport, $queryData, $name) | |
62 | { | |
63 | // we only support UDP right now | |
64 | if ($transport !== 'udp') { | |
65 | return Promise\reject(new \RuntimeException( | |
66 | 'DNS query for ' . $name . ' failed: Requested transport "' . $transport . '" not available, only UDP is supported in this version' | |
67 | )); | |
68 | } | |
69 | ||
70 | $that = $this; | |
71 | $parser = $this->parser; | |
72 | $loop = $this->loop; | |
73 | ||
74 | // UDP connections are instant, so try this without a timer | |
75 | try { | |
76 | $conn = $this->createConnection($nameserver, $transport); | |
77 | } catch (\Exception $e) { | |
78 | return Promise\reject(new \RuntimeException('DNS query for ' . $name . ' failed: ' . $e->getMessage(), 0, $e)); | |
79 | } | |
80 | ||
81 | $deferred = new Deferred(function ($resolve, $reject) use (&$timer, $loop, &$conn, $name) { | |
82 | $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name))); | |
83 | ||
84 | if ($timer !== null) { | |
85 | $loop->cancelTimer($timer); | |
86 | } | |
87 | $conn->close(); | |
88 | }); | |
89 | ||
90 | $timer = null; | |
91 | if ($this->timeout !== null) { | |
92 | $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { | |
93 | $conn->close(); | |
94 | $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); | |
95 | }); | |
96 | } | |
97 | ||
98 | $conn->on('data', function ($data) use ($conn, $parser, $deferred, $timer, $loop, $name) { | |
99 | $conn->end(); | |
100 | if ($timer !== null) { | |
101 | $loop->cancelTimer($timer); | |
102 | } | |
103 | ||
104 | try { | |
105 | $response = $parser->parseMessage($data); | |
106 | } catch (\Exception $e) { | |
107 | $deferred->reject($e); | |
108 | return; | |
109 | } | |
110 | ||
111 | if ($response->header->isTruncated()) { | |
112 | $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')); | |
113 | return; | |
114 | } | |
115 | ||
116 | $deferred->resolve($response); | |
117 | }); | |
118 | $conn->write($queryData); | |
119 | ||
120 | return $deferred->promise(); | |
121 | } | |
122 | ||
123 | /** | |
124 | * @deprecated unused, exists for BC only | |
125 | */ | |
126 | protected function generateId() | |
127 | { | |
128 | return mt_rand(0, 0xffff); | |
129 | } | |
130 | ||
131 | /** | |
132 | * @param string $nameserver | |
133 | * @param string $transport | |
134 | * @return \React\Stream\DuplexStreamInterface | |
135 | */ | |
136 | protected function createConnection($nameserver, $transport) | |
137 | { | |
138 | $fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); | |
139 | if ($fd === false) { | |
140 | throw new \RuntimeException('Unable to connect to DNS server: ' . $errstr, $errno); | |
141 | } | |
142 | ||
143 | // Instantiate stream instance around this stream resource. | |
144 | // This ought to be replaced with a datagram socket in the future. | |
145 | // Temporary work around for Windows 10: buffer whole UDP response | |
146 | // @coverageIgnoreStart | |
147 | if (!class_exists('React\Stream\Stream')) { | |
148 | // prefer DuplexResourceStream as of react/stream v0.7.0 | |
149 | $conn = new DuplexResourceStream($fd, $this->loop, -1); | |
150 | } else { | |
151 | // use legacy Stream class for react/stream < v0.7.0 | |
152 | $conn = new Stream($fd, $this->loop); | |
153 | $conn->bufferSize = null; | |
154 | } | |
155 | // @coverageIgnoreEnd | |
156 | ||
157 | return $conn; | |
158 | } | |
159 | } |
3 | 3 | |
4 | 4 | interface ExecutorInterface |
5 | 5 | { |
6 | public function query($nameserver, Query $query); | |
6 | /** | |
7 | * Executes a query and will return a response message | |
8 | * | |
9 | * It returns a Promise which either fulfills with a response | |
10 | * `React\Dns\Model\Message` on success or rejects with an `Exception` if | |
11 | * the query is not successful. A response message may indicate an error | |
12 | * condition in its `rcode`, but this is considered a valid response message. | |
13 | * | |
14 | * ```php | |
15 | * $executor->query($query)->then( | |
16 | * function (React\Dns\Model\Message $response) { | |
17 | * // response message successfully received | |
18 | * var_dump($response->rcode, $response->answers); | |
19 | * }, | |
20 | * function (Exception $error) { | |
21 | * // failed to query due to $error | |
22 | * } | |
23 | * ); | |
24 | * ``` | |
25 | * | |
26 | * The returned Promise MUST be implemented in such a way that it can be | |
27 | * cancelled when it is still pending. Cancelling a pending promise MUST | |
28 | * reject its value with an Exception. It SHOULD clean up any underlying | |
29 | * resources and references as applicable. | |
30 | * | |
31 | * ```php | |
32 | * $promise = $executor->query($query); | |
33 | * | |
34 | * $promise->cancel(); | |
35 | * ``` | |
36 | * | |
37 | * @param Query $query | |
38 | * @return \React\Promise\PromiseInterface<\React\Dns\Model\Message,\Exception> | |
39 | * resolves with response message on success or rejects with an Exception on error | |
40 | */ | |
41 | public function query(Query $query); | |
7 | 42 | } |
7 | 7 | use React\Promise; |
8 | 8 | |
9 | 9 | /** |
10 | * Resolves hosts from the givne HostsFile or falls back to another executor | |
10 | * Resolves hosts from the given HostsFile or falls back to another executor | |
11 | 11 | * |
12 | 12 | * If the host is found in the hosts file, it will not be passed to the actual |
13 | 13 | * DNS executor. If the host is not found in the hosts file, it will be passed |
14 | 14 | * to the DNS executor as a fallback. |
15 | 15 | */ |
16 | class HostsFileExecutor implements ExecutorInterface | |
16 | final class HostsFileExecutor implements ExecutorInterface | |
17 | 17 | { |
18 | 18 | private $hosts; |
19 | 19 | private $fallback; |
24 | 24 | $this->fallback = $fallback; |
25 | 25 | } |
26 | 26 | |
27 | public function query($nameserver, Query $query) | |
27 | public function query(Query $query) | |
28 | 28 | { |
29 | 29 | if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) { |
30 | 30 | // forward lookup for type A or AAAA |
60 | 60 | } |
61 | 61 | } |
62 | 62 | |
63 | return $this->fallback->query($nameserver, $query); | |
63 | return $this->fallback->query($query); | |
64 | 64 | } |
65 | 65 | |
66 | 66 | private function getIpFromHost($host) |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | class Query | |
4 | /** | |
5 | * This class represents a single question in a query/response message | |
6 | * | |
7 | * It uses a structure similar to `\React\Dns\Message\Record`, but does not | |
8 | * contain fields for resulting TTL and resulting record data (IPs etc.). | |
9 | * | |
10 | * @link https://tools.ietf.org/html/rfc1035#section-4.1.2 | |
11 | * @see \React\Dns\Message\Record | |
12 | */ | |
13 | final class Query | |
5 | 14 | { |
15 | /** | |
16 | * @var string query name, i.e. hostname to look up | |
17 | */ | |
6 | 18 | public $name; |
19 | ||
20 | /** | |
21 | * @var int query type (aka QTYPE), see Message::TYPE_* constants | |
22 | */ | |
7 | 23 | public $type; |
24 | ||
25 | /** | |
26 | * @var int query class (aka QCLASS), see Message::CLASS_IN constant | |
27 | */ | |
8 | 28 | public $class; |
9 | 29 | |
10 | 30 | /** |
11 | * @deprecated still used internally for BC reasons, should not be used externally. | |
31 | * @param string $name query name, i.e. hostname to look up | |
32 | * @param int $type query type, see Message::TYPE_* constants | |
33 | * @param int $class query class, see Message::CLASS_IN constant | |
12 | 34 | */ |
13 | public $currentTime; | |
14 | ||
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) | |
35 | public function __construct($name, $type, $class) | |
22 | 36 | { |
23 | if($currentTime === null) { | |
24 | $currentTime = time(); | |
25 | } | |
26 | ||
27 | 37 | $this->name = $name; |
28 | 38 | $this->type = $type; |
29 | 39 | $this->class = $class; |
30 | $this->currentTime = $currentTime; | |
31 | 40 | } |
32 | 41 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Record; | |
5 | ||
6 | class RecordBag | |
7 | { | |
8 | private $records = array(); | |
9 | ||
10 | public function set($currentTime, Record $record) | |
11 | { | |
12 | $this->records[] = array($currentTime + $record->ttl, $record); | |
13 | } | |
14 | ||
15 | public function all() | |
16 | { | |
17 | return array_values(array_map( | |
18 | function ($value) { | |
19 | list($expiresAt, $record) = $value; | |
20 | return $record; | |
21 | }, | |
22 | $this->records | |
23 | )); | |
24 | } | |
25 | } |
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 | use React\Promise\PromiseInterface; | |
9 | ||
10 | /** | |
11 | * Wraps an underlying cache interface and exposes only cached DNS data | |
12 | */ | |
13 | class RecordCache | |
14 | { | |
15 | private $cache; | |
16 | private $expiredAt; | |
17 | ||
18 | public function __construct(CacheInterface $cache) | |
19 | { | |
20 | $this->cache = $cache; | |
21 | } | |
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 | */ | |
30 | public function lookup(Query $query) | |
31 | { | |
32 | $id = $this->serializeQueryToIdentity($query); | |
33 | ||
34 | $expiredAt = $this->expiredAt; | |
35 | ||
36 | return $this->cache | |
37 | ->get($id) | |
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 */ | |
45 | $recordBag = unserialize($value); | |
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. | |
50 | if (null !== $expiredAt && $expiredAt <= $query->currentTime) { | |
51 | return Promise\reject(); | |
52 | } | |
53 | ||
54 | return $recordBag->all(); | |
55 | }); | |
56 | } | |
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 | */ | |
65 | public function storeResponseMessage($currentTime, Message $message) | |
66 | { | |
67 | foreach ($message->answers as $record) { | |
68 | $this->storeRecord($currentTime, $record); | |
69 | } | |
70 | } | |
71 | ||
72 | /** | |
73 | * Stores a single record from a response message in the cache | |
74 | * | |
75 | * @param int $currentTime | |
76 | * @param Record $record | |
77 | */ | |
78 | public function storeRecord($currentTime, Record $record) | |
79 | { | |
80 | $id = $this->serializeRecordToIdentity($record); | |
81 | ||
82 | $cache = $this->cache; | |
83 | ||
84 | $this->cache | |
85 | ->get($id) | |
86 | ->then( | |
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 | |
94 | return unserialize($value); | |
95 | }, | |
96 | function ($e) { | |
97 | // legacy cache < 0.5 cache miss rejects promise, return empty bag here | |
98 | return new RecordBag(); | |
99 | } | |
100 | ) | |
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 | |
103 | $recordBag->set($currentTime, $record); | |
104 | $cache->set($id, serialize($recordBag)); | |
105 | }); | |
106 | } | |
107 | ||
108 | public function expire($currentTime) | |
109 | { | |
110 | $this->expiredAt = $currentTime; | |
111 | } | |
112 | ||
113 | public function serializeQueryToIdentity(Query $query) | |
114 | { | |
115 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); | |
116 | } | |
117 | ||
118 | public function serializeRecordToIdentity(Record $record) | |
119 | { | |
120 | return sprintf('%s:%s:%s', $record->name, $record->type, $record->class); | |
121 | } | |
122 | } |
4 | 4 | use React\Promise\CancellablePromiseInterface; |
5 | 5 | use React\Promise\Deferred; |
6 | 6 | |
7 | class RetryExecutor implements ExecutorInterface | |
7 | final class RetryExecutor implements ExecutorInterface | |
8 | 8 | { |
9 | 9 | private $executor; |
10 | 10 | private $retries; |
15 | 15 | $this->retries = $retries; |
16 | 16 | } |
17 | 17 | |
18 | public function query($nameserver, Query $query) | |
18 | public function query(Query $query) | |
19 | 19 | { |
20 | return $this->tryQuery($nameserver, $query, $this->retries); | |
20 | return $this->tryQuery($query, $this->retries); | |
21 | 21 | } |
22 | 22 | |
23 | public function tryQuery($nameserver, Query $query, $retries) | |
23 | public function tryQuery(Query $query, $retries) | |
24 | 24 | { |
25 | 25 | $deferred = new Deferred(function () use (&$promise) { |
26 | 26 | if ($promise instanceof CancellablePromiseInterface) { |
34 | 34 | }; |
35 | 35 | |
36 | 36 | $executor = $this->executor; |
37 | $errorback = function ($e) use ($deferred, &$promise, $nameserver, $query, $success, &$errorback, &$retries, $executor) { | |
37 | $errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries, $executor) { | |
38 | 38 | if (!$e instanceof TimeoutException) { |
39 | 39 | $errorback = null; |
40 | 40 | $deferred->reject($e); |
61 | 61 | $r->setValue($e, $trace); |
62 | 62 | } else { |
63 | 63 | --$retries; |
64 | $promise = $executor->query($nameserver, $query)->then( | |
64 | $promise = $executor->query($query)->then( | |
65 | 65 | $success, |
66 | 66 | $errorback |
67 | 67 | ); |
68 | 68 | } |
69 | 69 | }; |
70 | 70 | |
71 | $promise = $this->executor->query($nameserver, $query)->then( | |
71 | $promise = $this->executor->query($query)->then( | |
72 | 72 | $success, |
73 | 73 | $errorback |
74 | 74 | ); |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Promise\Promise; | |
5 | ||
6 | /** | |
7 | * Send DNS queries over a UDP or TCP/IP stream transport. | |
8 | * | |
9 | * This class will automatically choose the correct transport protocol to send | |
10 | * a DNS query to your DNS server. It will always try to send it over the more | |
11 | * efficient UDP transport first. If this query yields a size related issue | |
12 | * (truncated messages), it will retry over a streaming TCP/IP transport. | |
13 | * | |
14 | * For more advanced usages one can utilize this class directly. | |
15 | * The following example looks up the `IPv6` address for `reactphp.org`. | |
16 | * | |
17 | * ```php | |
18 | * $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor); | |
19 | * | |
20 | * $executor->query( | |
21 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
22 | * )->then(function (Message $message) { | |
23 | * foreach ($message->answers as $answer) { | |
24 | * echo 'IPv6: ' . $answer->data . PHP_EOL; | |
25 | * } | |
26 | * }, 'printf'); | |
27 | * ``` | |
28 | * | |
29 | * Note that this executor only implements the logic to select the correct | |
30 | * transport for the given DNS query. Implementing the correct transport logic, | |
31 | * implementing timeouts and any retry logic is left up to the given executors, | |
32 | * see also [`UdpTransportExecutor`](#udptransportexecutor) and | |
33 | * [`TcpTransportExecutor`](#tcptransportexecutor) for more details. | |
34 | * | |
35 | * Note that this executor is entirely async and as such allows you to execute | |
36 | * any number of queries concurrently. You should probably limit the number of | |
37 | * concurrent queries in your application or you're very likely going to face | |
38 | * rate limitations and bans on the resolver end. For many common applications, | |
39 | * you may want to avoid sending the same query multiple times when the first | |
40 | * one is still pending, so you will likely want to use this in combination with | |
41 | * a `CoopExecutor` like this: | |
42 | * | |
43 | * ```php | |
44 | * $executor = new CoopExecutor( | |
45 | * new SelectiveTransportExecutor( | |
46 | * $datagramExecutor, | |
47 | * $streamExecutor | |
48 | * ) | |
49 | * ); | |
50 | * ``` | |
51 | */ | |
52 | class SelectiveTransportExecutor implements ExecutorInterface | |
53 | { | |
54 | private $datagramExecutor; | |
55 | private $streamExecutor; | |
56 | ||
57 | public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor) | |
58 | { | |
59 | $this->datagramExecutor = $datagramExecutor; | |
60 | $this->streamExecutor = $streamExecutor; | |
61 | } | |
62 | ||
63 | public function query(Query $query) | |
64 | { | |
65 | $stream = $this->streamExecutor; | |
66 | $pending = $this->datagramExecutor->query($query); | |
67 | ||
68 | return new Promise(function ($resolve, $reject) use (&$pending, $stream, $query) { | |
69 | $pending->then( | |
70 | $resolve, | |
71 | function ($e) use (&$pending, $stream, $query, $resolve, $reject) { | |
72 | if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) { | |
73 | $pending = $stream->query($query)->then($resolve, $reject); | |
74 | } else { | |
75 | $reject($e); | |
76 | } | |
77 | } | |
78 | ); | |
79 | }, function () use (&$pending) { | |
80 | $pending->cancel(); | |
81 | $pending = null; | |
82 | }); | |
83 | } | |
84 | } |
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 TCP/IP stream transport. | |
12 | * | |
13 | * This is one of the main classes that send a DNS query to your DNS server. | |
14 | * | |
15 | * For more advanced usages one can utilize this class directly. | |
16 | * The following example looks up the `IPv6` address for `reactphp.org`. | |
17 | * | |
18 | * ```php | |
19 | * $loop = Factory::create(); | |
20 | * $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
21 | * | |
22 | * $executor->query( | |
23 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
24 | * )->then(function (Message $message) { | |
25 | * foreach ($message->answers as $answer) { | |
26 | * echo 'IPv6: ' . $answer->data . PHP_EOL; | |
27 | * } | |
28 | * }, 'printf'); | |
29 | * | |
30 | * $loop->run(); | |
31 | * ``` | |
32 | * | |
33 | * See also [example #92](examples). | |
34 | * | |
35 | * Note that this executor does not implement a timeout, so you will very likely | |
36 | * want to use this in combination with a `TimeoutExecutor` like this: | |
37 | * | |
38 | * ```php | |
39 | * $executor = new TimeoutExecutor( | |
40 | * new TcpTransportExecutor($nameserver, $loop), | |
41 | * 3.0, | |
42 | * $loop | |
43 | * ); | |
44 | * ``` | |
45 | * | |
46 | * Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP | |
47 | * transport, so you do not necessarily have to implement any retry logic. | |
48 | * | |
49 | * Note that this executor is entirely async and as such allows you to execute | |
50 | * queries concurrently. The first query will establish a TCP/IP socket | |
51 | * connection to the DNS server which will be kept open for a short period. | |
52 | * Additional queries will automatically reuse this existing socket connection | |
53 | * to the DNS server, will pipeline multiple requests over this single | |
54 | * connection and will keep an idle connection open for a short period. The | |
55 | * initial TCP/IP connection overhead may incur a slight delay if you only send | |
56 | * occasional queries – when sending a larger number of concurrent queries over | |
57 | * an existing connection, it becomes increasingly more efficient and avoids | |
58 | * creating many concurrent sockets like the UDP-based executor. You may still | |
59 | * want to limit the number of (concurrent) queries in your application or you | |
60 | * may be facing rate limitations and bans on the resolver end. For many common | |
61 | * applications, you may want to avoid sending the same query multiple times | |
62 | * when the first one is still pending, so you will likely want to use this in | |
63 | * combination with a `CoopExecutor` like this: | |
64 | * | |
65 | * ```php | |
66 | * $executor = new CoopExecutor( | |
67 | * new TimeoutExecutor( | |
68 | * new TcpTransportExecutor($nameserver, $loop), | |
69 | * 3.0, | |
70 | * $loop | |
71 | * ) | |
72 | * ); | |
73 | * ``` | |
74 | * | |
75 | * > Internally, this class uses PHP's TCP/IP sockets and does not take advantage | |
76 | * of [react/socket](https://github.com/reactphp/socket) purely for | |
77 | * organizational reasons to avoid a cyclic dependency between the two | |
78 | * packages. Higher-level components should take advantage of the Socket | |
79 | * component instead of reimplementing this socket logic from scratch. | |
80 | */ | |
81 | class TcpTransportExecutor implements ExecutorInterface | |
82 | { | |
83 | private $nameserver; | |
84 | private $loop; | |
85 | private $parser; | |
86 | private $dumper; | |
87 | ||
88 | /** | |
89 | * @var ?resource | |
90 | */ | |
91 | private $socket; | |
92 | ||
93 | /** | |
94 | * @var Deferred[] | |
95 | */ | |
96 | private $pending = array(); | |
97 | ||
98 | /** | |
99 | * @var string[] | |
100 | */ | |
101 | private $names = array(); | |
102 | ||
103 | /** | |
104 | * Maximum idle time when socket is current unused (i.e. no pending queries outstanding) | |
105 | * | |
106 | * If a new query is to be sent during the idle period, we can reuse the | |
107 | * existing socket without having to wait for a new socket connection. | |
108 | * This uses a rather small, hard-coded value to not keep any unneeded | |
109 | * sockets open and to not keep the loop busy longer than needed. | |
110 | * | |
111 | * A future implementation may take advantage of `edns-tcp-keepalive` to keep | |
112 | * the socket open for longer periods. This will likely require explicit | |
113 | * configuration because this may consume additional resources and also keep | |
114 | * the loop busy for longer than expected in some applications. | |
115 | * | |
116 | * @var float | |
117 | * @link https://tools.ietf.org/html/rfc7766#section-6.2.1 | |
118 | * @link https://tools.ietf.org/html/rfc7828 | |
119 | */ | |
120 | private $idlePeriod = 0.001; | |
121 | ||
122 | /** | |
123 | * @var ?\React\EventLoop\TimerInterface | |
124 | */ | |
125 | private $idleTimer; | |
126 | ||
127 | private $writeBuffer = ''; | |
128 | private $writePending = false; | |
129 | ||
130 | private $readBuffer = ''; | |
131 | private $readPending = false; | |
132 | ||
133 | /** | |
134 | * @param string $nameserver | |
135 | * @param LoopInterface $loop | |
136 | */ | |
137 | public function __construct($nameserver, LoopInterface $loop) | |
138 | { | |
139 | if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) { | |
140 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
141 | $nameserver = '[' . $nameserver . ']'; | |
142 | } | |
143 | ||
144 | $parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver); | |
145 | if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || !\filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { | |
146 | throw new \InvalidArgumentException('Invalid nameserver address given'); | |
147 | } | |
148 | ||
149 | $this->nameserver = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); | |
150 | $this->loop = $loop; | |
151 | $this->parser = new Parser(); | |
152 | $this->dumper = new BinaryDumper(); | |
153 | } | |
154 | ||
155 | public function query(Query $query) | |
156 | { | |
157 | $request = Message::createRequestForQuery($query); | |
158 | ||
159 | // keep shuffing message ID to avoid using the same message ID for two pending queries at the same time | |
160 | while (isset($this->pending[$request->id])) { | |
161 | $request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore | |
162 | } | |
163 | ||
164 | $queryData = $this->dumper->toBinary($request); | |
165 | $length = \strlen($queryData); | |
166 | if ($length > 0xffff) { | |
167 | return \React\Promise\reject(new \RuntimeException( | |
168 | 'DNS query for ' . $query->name . ' failed: Query too large for TCP transport' | |
169 | )); | |
170 | } | |
171 | ||
172 | $queryData = \pack('n', $length) . $queryData; | |
173 | ||
174 | if ($this->socket === null) { | |
175 | // create async TCP/IP connection (may take a while) | |
176 | $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT); | |
177 | if ($socket === false) { | |
178 | return \React\Promise\reject(new \RuntimeException( | |
179 | 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', | |
180 | $errno | |
181 | )); | |
182 | } | |
183 | ||
184 | // set socket to non-blocking and wait for it to become writable (connection success/rejected) | |
185 | \stream_set_blocking($socket, false); | |
186 | $this->socket = $socket; | |
187 | } | |
188 | ||
189 | if ($this->idleTimer !== null) { | |
190 | $this->loop->cancelTimer($this->idleTimer); | |
191 | $this->idleTimer = null; | |
192 | } | |
193 | ||
194 | // wait for socket to become writable to actually write out data | |
195 | $this->writeBuffer .= $queryData; | |
196 | if (!$this->writePending) { | |
197 | $this->writePending = true; | |
198 | $this->loop->addWriteStream($this->socket, array($this, 'handleWritable')); | |
199 | } | |
200 | ||
201 | $names =& $this->names; | |
202 | $that = $this; | |
203 | $deferred = new Deferred(function () use ($that, &$names, $request) { | |
204 | // remove from list of pending names, but remember pending query | |
205 | $name = $names[$request->id]; | |
206 | unset($names[$request->id]); | |
207 | $that->checkIdle(); | |
208 | ||
209 | throw new CancellationException('DNS query for ' . $name . ' has been cancelled'); | |
210 | }); | |
211 | ||
212 | $this->pending[$request->id] = $deferred; | |
213 | $this->names[$request->id] = $query->name; | |
214 | ||
215 | return $deferred->promise(); | |
216 | } | |
217 | ||
218 | /** | |
219 | * @internal | |
220 | */ | |
221 | public function handleWritable() | |
222 | { | |
223 | if ($this->readPending === false) { | |
224 | $name = @\stream_socket_get_name($this->socket, true); | |
225 | if ($name === false) { | |
226 | $this->closeError('Connection to DNS server rejected'); | |
227 | return; | |
228 | } | |
229 | ||
230 | $this->readPending = true; | |
231 | $this->loop->addReadStream($this->socket, array($this, 'handleRead')); | |
232 | } | |
233 | ||
234 | $written = @\fwrite($this->socket, $this->writeBuffer); | |
235 | if ($written === false || $written === 0) { | |
236 | $this->closeError('Unable to write to closed socket'); | |
237 | return; | |
238 | } | |
239 | ||
240 | if (isset($this->writeBuffer[$written])) { | |
241 | $this->writeBuffer = \substr($this->writeBuffer, $written); | |
242 | } else { | |
243 | $this->loop->removeWriteStream($this->socket); | |
244 | $this->writePending = false; | |
245 | $this->writeBuffer = ''; | |
246 | } | |
247 | } | |
248 | ||
249 | /** | |
250 | * @internal | |
251 | */ | |
252 | public function handleRead() | |
253 | { | |
254 | // read one chunk of data from the DNS server | |
255 | // any error is fatal, this is a stream of TCP/IP data | |
256 | $chunk = @\fread($this->socket, 65536); | |
257 | if ($chunk === false || $chunk === '') { | |
258 | $this->closeError('Connection to DNS server lost'); | |
259 | return; | |
260 | } | |
261 | ||
262 | // reassemble complete message by concatenating all chunks. | |
263 | $this->readBuffer .= $chunk; | |
264 | ||
265 | // response message header contains at least 12 bytes | |
266 | while (isset($this->readBuffer[11])) { | |
267 | // read response message length from first 2 bytes and ensure we have length + data in buffer | |
268 | list(, $length) = \unpack('n', $this->readBuffer); | |
269 | if (!isset($this->readBuffer[$length + 1])) { | |
270 | return; | |
271 | } | |
272 | ||
273 | $data = \substr($this->readBuffer, 2, $length); | |
274 | $this->readBuffer = (string)substr($this->readBuffer, $length + 2); | |
275 | ||
276 | try { | |
277 | $response = $this->parser->parseMessage($data); | |
278 | } catch (\Exception $e) { | |
279 | // reject all pending queries if we received an invalid message from remote server | |
280 | $this->closeError('Invalid message received from DNS server'); | |
281 | return; | |
282 | } | |
283 | ||
284 | // reject all pending queries if we received an unexpected response ID or truncated response | |
285 | if (!isset($this->pending[$response->id]) || $response->tc) { | |
286 | $this->closeError('Invalid response message received from DNS server'); | |
287 | return; | |
288 | } | |
289 | ||
290 | $deferred = $this->pending[$response->id]; | |
291 | unset($this->pending[$response->id], $this->names[$response->id]); | |
292 | ||
293 | $deferred->resolve($response); | |
294 | ||
295 | $this->checkIdle(); | |
296 | } | |
297 | } | |
298 | ||
299 | /** | |
300 | * @internal | |
301 | * @param string $reason | |
302 | */ | |
303 | public function closeError($reason) | |
304 | { | |
305 | $this->readBuffer = ''; | |
306 | if ($this->readPending) { | |
307 | $this->loop->removeReadStream($this->socket); | |
308 | $this->readPending = false; | |
309 | } | |
310 | ||
311 | $this->writeBuffer = ''; | |
312 | if ($this->writePending) { | |
313 | $this->loop->removeWriteStream($this->socket); | |
314 | $this->writePending = false; | |
315 | } | |
316 | ||
317 | if ($this->idleTimer !== null) { | |
318 | $this->loop->cancelTimer($this->idleTimer); | |
319 | $this->idleTimer = null; | |
320 | } | |
321 | ||
322 | @\fclose($this->socket); | |
323 | $this->socket = null; | |
324 | ||
325 | foreach ($this->names as $id => $name) { | |
326 | $this->pending[$id]->reject(new \RuntimeException( | |
327 | 'DNS query for ' . $name . ' failed: ' . $reason | |
328 | )); | |
329 | } | |
330 | $this->pending = $this->names = array(); | |
331 | } | |
332 | ||
333 | /** | |
334 | * @internal | |
335 | */ | |
336 | public function checkIdle() | |
337 | { | |
338 | if ($this->idleTimer === null && !$this->names) { | |
339 | $that = $this; | |
340 | $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) { | |
341 | $that->closeError('Idle timeout'); | |
342 | }); | |
343 | } | |
344 | } | |
345 | } |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | class TimeoutException extends \Exception | |
4 | final class TimeoutException extends \Exception | |
5 | 5 | { |
6 | 6 | } |
6 | 6 | use React\Promise\CancellablePromiseInterface; |
7 | 7 | use React\Promise\Timer; |
8 | 8 | |
9 | class TimeoutExecutor implements ExecutorInterface | |
9 | final class TimeoutExecutor implements ExecutorInterface | |
10 | 10 | { |
11 | 11 | private $executor; |
12 | 12 | private $loop; |
19 | 19 | $this->timeout = $timeout; |
20 | 20 | } |
21 | 21 | |
22 | public function query($nameserver, Query $query) | |
22 | public function query(Query $query) | |
23 | 23 | { |
24 | return Timer\timeout($this->executor->query($nameserver, $query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { | |
24 | return Timer\timeout($this->executor->query($query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { | |
25 | 25 | if ($e instanceof Timer\TimeoutException) { |
26 | 26 | $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e); |
27 | 27 | } |
18 | 18 | * |
19 | 19 | * ```php |
20 | 20 | * $loop = Factory::create(); |
21 | * $executor = new UdpTransportExecutor($loop); | |
21 | * $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
22 | 22 | * |
23 | 23 | * $executor->query( |
24 | * '8.8.8.8:53', | |
25 | 24 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) |
26 | 25 | * )->then(function (Message $message) { |
27 | 26 | * foreach ($message->answers as $answer) { |
39 | 38 | * |
40 | 39 | * ```php |
41 | 40 | * $executor = new TimeoutExecutor( |
42 | * new UdpTransportExecutor($loop), | |
41 | * new UdpTransportExecutor($nameserver, $loop), | |
43 | 42 | * 3.0, |
44 | 43 | * $loop |
45 | 44 | * ); |
52 | 51 | * ```php |
53 | 52 | * $executor = new RetryExecutor( |
54 | 53 | * new TimeoutExecutor( |
55 | * new UdpTransportExecutor($loop), | |
54 | * new UdpTransportExecutor($nameserver, $loop), | |
56 | 55 | * 3.0, |
57 | 56 | * $loop |
57 | * ) | |
58 | * ); | |
59 | * ``` | |
60 | * | |
61 | * Note that this executor is entirely async and as such allows you to execute | |
62 | * any number of queries concurrently. You should probably limit the number of | |
63 | * concurrent queries in your application or you're very likely going to face | |
64 | * rate limitations and bans on the resolver end. For many common applications, | |
65 | * you may want to avoid sending the same query multiple times when the first | |
66 | * one is still pending, so you will likely want to use this in combination with | |
67 | * a `CoopExecutor` like this: | |
68 | * | |
69 | * ```php | |
70 | * $executor = new CoopExecutor( | |
71 | * new RetryExecutor( | |
72 | * new TimeoutExecutor( | |
73 | * new UdpTransportExecutor($nameserver, $loop), | |
74 | * 3.0, | |
75 | * $loop | |
76 | * ) | |
58 | 77 | * ) |
59 | 78 | * ); |
60 | 79 | * ``` |
65 | 84 | * packages. Higher-level components should take advantage of the Datagram |
66 | 85 | * component instead of reimplementing this socket logic from scratch. |
67 | 86 | */ |
68 | class UdpTransportExecutor implements ExecutorInterface | |
87 | final class UdpTransportExecutor implements ExecutorInterface | |
69 | 88 | { |
89 | private $nameserver; | |
70 | 90 | private $loop; |
71 | 91 | private $parser; |
72 | 92 | private $dumper; |
73 | 93 | |
74 | 94 | /** |
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 | |
95 | * @param string $nameserver | |
96 | * @param LoopInterface $loop | |
78 | 97 | */ |
79 | public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null) | |
98 | public function __construct($nameserver, LoopInterface $loop) | |
80 | 99 | { |
81 | if ($parser === null) { | |
82 | $parser = new Parser(); | |
83 | } | |
84 | if ($dumper === null) { | |
85 | $dumper = new BinaryDumper(); | |
100 | if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) { | |
101 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
102 | $nameserver = '[' . $nameserver . ']'; | |
86 | 103 | } |
87 | 104 | |
105 | $parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver); | |
106 | if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || !\filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { | |
107 | throw new \InvalidArgumentException('Invalid nameserver address given'); | |
108 | } | |
109 | ||
110 | $this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); | |
88 | 111 | $this->loop = $loop; |
89 | $this->parser = $parser; | |
90 | $this->dumper = $dumper; | |
112 | $this->parser = new Parser(); | |
113 | $this->dumper = new BinaryDumper(); | |
91 | 114 | } |
92 | 115 | |
93 | public function query($nameserver, Query $query) | |
116 | public function query(Query $query) | |
94 | 117 | { |
95 | 118 | $request = Message::createRequestForQuery($query); |
96 | 119 | |
97 | 120 | $queryData = $this->dumper->toBinary($request); |
98 | 121 | if (isset($queryData[512])) { |
99 | 122 | return \React\Promise\reject(new \RuntimeException( |
100 | 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport' | |
123 | 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport', | |
124 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 | |
101 | 125 | )); |
102 | 126 | } |
103 | 127 | |
104 | 128 | // UDP connections are instant, so try connection without a loop or timeout |
105 | $socket = @\stream_socket_client("udp://$nameserver", $errno, $errstr, 0); | |
129 | $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0); | |
106 | 130 | if ($socket === false) { |
107 | 131 | return \React\Promise\reject(new \RuntimeException( |
108 | 132 | 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', |
139 | 163 | |
140 | 164 | // ignore and await next if we received an unexpected response ID |
141 | 165 | // this may as well be a fake response from an attacker (possible cache poisoning) |
142 | if ($response->getId() !== $request->getId()) { | |
166 | if ($response->id !== $request->id) { | |
143 | 167 | return; |
144 | 168 | } |
145 | 169 | |
147 | 171 | $loop->removeReadStream($socket); |
148 | 172 | \fclose($socket); |
149 | 173 | |
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')); | |
174 | if ($response->tc) { | |
175 | $deferred->reject(new \RuntimeException( | |
176 | 'DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query', | |
177 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 | |
178 | )); | |
152 | 179 | return; |
153 | 180 | } |
154 | 181 |
1 | 1 | |
2 | 2 | namespace React\Dns; |
3 | 3 | |
4 | class RecordNotFoundException extends \Exception | |
4 | final class RecordNotFoundException extends \Exception | |
5 | 5 | { |
6 | 6 | } |
4 | 4 | use React\Cache\ArrayCache; |
5 | 5 | use React\Cache\CacheInterface; |
6 | 6 | use React\Dns\Config\HostsFile; |
7 | use React\Dns\Query\CachedExecutor; | |
7 | use React\Dns\Query\CachingExecutor; | |
8 | use React\Dns\Query\CoopExecutor; | |
8 | 9 | use React\Dns\Query\ExecutorInterface; |
9 | 10 | use React\Dns\Query\HostsFileExecutor; |
10 | use React\Dns\Query\RecordCache; | |
11 | 11 | use React\Dns\Query\RetryExecutor; |
12 | use React\Dns\Query\SelectiveTransportExecutor; | |
13 | use React\Dns\Query\TcpTransportExecutor; | |
12 | 14 | use React\Dns\Query\TimeoutExecutor; |
13 | 15 | use React\Dns\Query\UdpTransportExecutor; |
14 | 16 | use React\EventLoop\LoopInterface; |
15 | 17 | |
16 | class Factory | |
18 | final class Factory | |
17 | 19 | { |
20 | /** | |
21 | * @param string $nameserver | |
22 | * @param LoopInterface $loop | |
23 | * @return \React\Dns\Resolver\ResolverInterface | |
24 | */ | |
18 | 25 | public function create($nameserver, LoopInterface $loop) |
19 | 26 | { |
20 | $nameserver = $this->addPortToServerIfMissing($nameserver); | |
21 | $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop)); | |
27 | $executor = $this->decorateHostsFileExecutor($this->createExecutor($nameserver, $loop)); | |
22 | 28 | |
23 | return new Resolver($nameserver, $executor); | |
29 | return new Resolver($executor); | |
24 | 30 | } |
25 | 31 | |
32 | /** | |
33 | * @param string $nameserver | |
34 | * @param LoopInterface $loop | |
35 | * @param ?CacheInterface $cache | |
36 | * @return \React\Dns\Resolver\ResolverInterface | |
37 | */ | |
26 | 38 | public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null) |
27 | 39 | { |
40 | // default to keeping maximum of 256 responses in cache unless explicitly given | |
28 | 41 | if (!($cache instanceof CacheInterface)) { |
29 | $cache = new ArrayCache(); | |
42 | $cache = new ArrayCache(256); | |
30 | 43 | } |
31 | 44 | |
32 | $nameserver = $this->addPortToServerIfMissing($nameserver); | |
33 | $executor = $this->decorateHostsFileExecutor($this->createCachedExecutor($loop, $cache)); | |
45 | $executor = $this->createExecutor($nameserver, $loop); | |
46 | $executor = new CachingExecutor($executor, $cache); | |
47 | $executor = $this->decorateHostsFileExecutor($executor); | |
34 | 48 | |
35 | return new Resolver($nameserver, $executor); | |
49 | return new Resolver($executor); | |
36 | 50 | } |
37 | 51 | |
38 | 52 | /** |
65 | 79 | return $executor; |
66 | 80 | } |
67 | 81 | |
68 | protected function createExecutor(LoopInterface $loop) | |
82 | private function createExecutor($nameserver, LoopInterface $loop) | |
83 | { | |
84 | $parts = \parse_url($nameserver); | |
85 | ||
86 | if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') { | |
87 | $executor = $this->createTcpExecutor($nameserver, $loop); | |
88 | } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') { | |
89 | $executor = $this->createUdpExecutor($nameserver, $loop); | |
90 | } else { | |
91 | $executor = new SelectiveTransportExecutor( | |
92 | $this->createUdpExecutor($nameserver, $loop), | |
93 | $this->createTcpExecutor($nameserver, $loop) | |
94 | ); | |
95 | } | |
96 | ||
97 | return new CoopExecutor($executor); | |
98 | } | |
99 | ||
100 | private function createTcpExecutor($nameserver, LoopInterface $loop) | |
69 | 101 | { |
70 | 102 | return new TimeoutExecutor( |
71 | new UdpTransportExecutor($loop), | |
103 | new TcpTransportExecutor($nameserver, $loop), | |
72 | 104 | 5.0, |
73 | 105 | $loop |
74 | 106 | ); |
75 | 107 | } |
76 | 108 | |
77 | protected function createRetryExecutor(LoopInterface $loop) | |
109 | private function createUdpExecutor($nameserver, LoopInterface $loop) | |
78 | 110 | { |
79 | return new RetryExecutor($this->createExecutor($loop)); | |
80 | } | |
81 | ||
82 | protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache) | |
83 | { | |
84 | return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache)); | |
85 | } | |
86 | ||
87 | protected function addPortToServerIfMissing($nameserver) | |
88 | { | |
89 | if (strpos($nameserver, '[') === false && substr_count($nameserver, ':') >= 2) { | |
90 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
91 | $nameserver = '[' . $nameserver . ']'; | |
92 | } | |
93 | // assume a dummy scheme when checking for the port, otherwise parse_url() fails | |
94 | if (parse_url('dummy://' . $nameserver, PHP_URL_PORT) === null) { | |
95 | $nameserver .= ':53'; | |
96 | } | |
97 | ||
98 | return $nameserver; | |
111 | return new RetryExecutor( | |
112 | new TimeoutExecutor( | |
113 | new UdpTransportExecutor( | |
114 | $nameserver, | |
115 | $loop | |
116 | ), | |
117 | 5.0, | |
118 | $loop | |
119 | ) | |
120 | ); | |
99 | 121 | } |
100 | 122 | } |
5 | 5 | use React\Dns\Query\ExecutorInterface; |
6 | 6 | use React\Dns\Query\Query; |
7 | 7 | use React\Dns\RecordNotFoundException; |
8 | use React\Promise\PromiseInterface; | |
9 | 8 | |
10 | class Resolver | |
9 | /** | |
10 | * @see ResolverInterface for the base interface | |
11 | */ | |
12 | final class Resolver implements ResolverInterface | |
11 | 13 | { |
12 | private $nameserver; | |
13 | 14 | private $executor; |
14 | 15 | |
15 | public function __construct($nameserver, ExecutorInterface $executor) | |
16 | public function __construct(ExecutorInterface $executor) | |
16 | 17 | { |
17 | $this->nameserver = $nameserver; | |
18 | 18 | $this->executor = $executor; |
19 | 19 | } |
20 | 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 | */ | |
59 | 21 | public function resolve($domain) |
60 | 22 | { |
61 | 23 | return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) { |
63 | 25 | }); |
64 | 26 | } |
65 | 27 | |
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 | 28 | public function resolveAll($domain, $type) |
113 | 29 | { |
114 | 30 | $query = new Query($domain, $type, Message::CLASS_IN); |
115 | 31 | $that = $this; |
116 | 32 | |
117 | 33 | return $this->executor->query( |
118 | $this->nameserver, | |
119 | 34 | $query |
120 | 35 | )->then(function (Message $response) use ($query, $that) { |
121 | 36 | return $that->extractValues($query, $response); |
122 | 37 | }); |
123 | } | |
124 | ||
125 | /** | |
126 | * @deprecated unused, exists for BC only | |
127 | */ | |
128 | public function extractAddress(Query $query, Message $response) | |
129 | { | |
130 | $addresses = $this->extractValues($query, $response); | |
131 | ||
132 | return $addresses[array_rand($addresses)]; | |
133 | 38 | } |
134 | 39 | |
135 | 40 | /** |
144 | 49 | public function extractValues(Query $query, Message $response) |
145 | 50 | { |
146 | 51 | // reject if response code indicates this is an error response message |
147 | $code = $response->getResponseCode(); | |
52 | $code = $response->rcode; | |
148 | 53 | if ($code !== Message::RCODE_OK) { |
149 | 54 | switch ($code) { |
150 | 55 | case Message::RCODE_FORMAT_ERROR: |
182 | 87 | } |
183 | 88 | |
184 | 89 | return array_values($addresses); |
185 | } | |
186 | ||
187 | /** | |
188 | * @deprecated unused, exists for BC only | |
189 | */ | |
190 | public function resolveAliases(array $answers, $name) | |
191 | { | |
192 | return $this->valuesByNameAndType($answers, $name, Message::TYPE_A); | |
193 | 90 | } |
194 | 91 | |
195 | 92 | /** |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Resolver; | |
3 | ||
4 | interface ResolverInterface | |
5 | { | |
6 | /** | |
7 | * Resolves the given $domain name to a single IPv4 address (type `A` query). | |
8 | * | |
9 | * ```php | |
10 | * $resolver->resolve('reactphp.org')->then(function ($ip) { | |
11 | * echo 'IP for reactphp.org is ' . $ip . PHP_EOL; | |
12 | * }); | |
13 | * ``` | |
14 | * | |
15 | * This is one of the main methods in this package. It sends a DNS query | |
16 | * for the given $domain name to your DNS server and returns a single IP | |
17 | * address on success. | |
18 | * | |
19 | * If the DNS server sends a DNS response message that contains more than | |
20 | * one IP address for this query, it will randomly pick one of the IP | |
21 | * addresses from the response. If you want the full list of IP addresses | |
22 | * or want to send a different type of query, you should use the | |
23 | * [`resolveAll()`](#resolveall) method instead. | |
24 | * | |
25 | * If the DNS server sends a DNS response message that indicates an error | |
26 | * code, this method will reject with a `RecordNotFoundException`. Its | |
27 | * message and code can be used to check for the response code. | |
28 | * | |
29 | * If the DNS communication fails and the server does not respond with a | |
30 | * valid response message, this message will reject with an `Exception`. | |
31 | * | |
32 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
33 | * | |
34 | * ```php | |
35 | * $promise = $resolver->resolve('reactphp.org'); | |
36 | * | |
37 | * $promise->cancel(); | |
38 | * ``` | |
39 | * | |
40 | * @param string $domain | |
41 | * @return \React\Promise\PromiseInterface<string,\Exception> | |
42 | * resolves with a single IP address on success or rejects with an Exception on error. | |
43 | */ | |
44 | public function resolve($domain); | |
45 | ||
46 | /** | |
47 | * Resolves all record values for the given $domain name and query $type. | |
48 | * | |
49 | * ```php | |
50 | * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { | |
51 | * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
52 | * }); | |
53 | * | |
54 | * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { | |
55 | * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
56 | * }); | |
57 | * ``` | |
58 | * | |
59 | * This is one of the main methods in this package. It sends a DNS query | |
60 | * for the given $domain name to your DNS server and returns a list with all | |
61 | * record values on success. | |
62 | * | |
63 | * If the DNS server sends a DNS response message that contains one or more | |
64 | * records for this query, it will return a list with all record values | |
65 | * from the response. You can use the `Message::TYPE_*` constants to control | |
66 | * which type of query will be sent. Note that this method always returns a | |
67 | * list of record values, but each record value type depends on the query | |
68 | * type. For example, it returns the IPv4 addresses for type `A` queries, | |
69 | * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, | |
70 | * `CNAME` and `PTR` queries and structured data for other queries. See also | |
71 | * the `Record` documentation for more details. | |
72 | * | |
73 | * If the DNS server sends a DNS response message that indicates an error | |
74 | * code, this method will reject with a `RecordNotFoundException`. Its | |
75 | * message and code can be used to check for the response code. | |
76 | * | |
77 | * If the DNS communication fails and the server does not respond with a | |
78 | * valid response message, this message will reject with an `Exception`. | |
79 | * | |
80 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
81 | * | |
82 | * ```php | |
83 | * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); | |
84 | * | |
85 | * $promise->cancel(); | |
86 | * ``` | |
87 | * | |
88 | * @param string $domain | |
89 | * @return \React\Promise\PromiseInterface<array,\Exception> | |
90 | * Resolves with all record values on success or rejects with an Exception on error. | |
91 | */ | |
92 | public function resolveAll($domain, $type); | |
93 | } |
98 | 98 | public function testLoadsFromWmicOnWindows() |
99 | 99 | { |
100 | 100 | if (DIRECTORY_SEPARATOR !== '\\') { |
101 | $this->markTestSkipped('Only on Windows'); | |
101 | // WMIC is Windows-only tool and not supported on other platforms | |
102 | // Unix is our main platform, so we don't want to report a skipped test here (yellow) | |
103 | // $this->markTestSkipped('Only on Windows'); | |
104 | $this->expectOutputString(''); | |
105 | return; | |
102 | 106 | } |
103 | 107 | |
104 | 108 | $config = Config::loadWmicBlocking(); |
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 | } |
46 | 46 | /** |
47 | 47 | * @group internet |
48 | 48 | */ |
49 | public function testResolveGoogleOverUdpResolves() | |
50 | { | |
51 | $factory = new Factory($this->loop); | |
52 | $this->resolver = $factory->create('udp://8.8.8.8', $this->loop); | |
53 | ||
54 | $promise = $this->resolver->resolve('google.com'); | |
55 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); | |
56 | ||
57 | $this->loop->run(); | |
58 | } | |
59 | ||
60 | /** | |
61 | * @group internet | |
62 | */ | |
63 | public function testResolveGoogleOverTcpResolves() | |
64 | { | |
65 | $factory = new Factory($this->loop); | |
66 | $this->resolver = $factory->create('tcp://8.8.8.8', $this->loop); | |
67 | ||
68 | $promise = $this->resolver->resolve('google.com'); | |
69 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); | |
70 | ||
71 | $this->loop->run(); | |
72 | } | |
73 | ||
74 | /** | |
75 | * @group internet | |
76 | */ | |
49 | 77 | public function testResolveAllGoogleMxResolvesWithCache() |
50 | 78 | { |
51 | 79 | $factory = new Factory(); |
52 | 80 | $this->resolver = $factory->createCached('8.8.8.8', $this->loop); |
53 | 81 | |
54 | 82 | $promise = $this->resolver->resolveAll('google.com', Message::TYPE_MX); |
83 | $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); | |
84 | ||
85 |