Package list reactphp-dns / 9b02e14
New upstream version 1.2.0 Dominik George 1 year, 4 months ago
58 changed file(s) with 4227 addition(s) and 2264 deletion(s). Raw diff Collapse all Expand all
77 - 7.0
88 - 7.1
99 - 7.2
10 - hhvm # ignore errors, see below
10 - 7.3
11 # - hhvm # requires legacy phpunit & ignore errors, see below
1112
1213 # lock distro so new future defaults will not break the build
1314 dist: trusty
1617 include:
1718 - php: 5.3
1819 dist: precise
20 - php: hhvm
21 install: composer require phpunit/phpunit:^5 --dev --no-interaction
1922 allow_failures:
2023 - php: hhvm
2124
00 # 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)
186
287 ## 0.4.16 (2018-11-11)
388
1212 * [Basic usage](#basic-usage)
1313 * [Caching](#caching)
1414 * [Custom cache adapter](#custom-cache-adapter)
15 * [Resolver](#resolver)
15 * [ResolverInterface](#resolverinterface)
1616 * [resolve()](#resolve)
1717 * [resolveAll()](#resolveall)
1818 * [Advanced usage](#advanced-usage)
1919 * [UdpTransportExecutor](#udptransportexecutor)
20 * [TcpTransportExecutor](#tcptransportexecutor)
21 * [SelectiveTransportExecutor](#selectivetransportexecutor)
2022 * [HostsFileExecutor](#hostsfileexecutor)
2123 * [Install](#install)
2224 * [Tests](#tests)
110112
111113 See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
112114
113 ## Resolver
115 ## ResolverInterface
116
117 <a id="resolver"><!-- legacy reference --></a>
114118
115119 ### resolve()
116120
207211
208212 ```php
209213 $loop = Factory::create();
210 $executor = new UdpTransportExecutor($loop);
214 $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
211215
212216 $executor->query(
213 '8.8.8.8:53',
214217 new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
215218 )->then(function (Message $message) {
216219 foreach ($message->answers as $answer) {
228231
229232 ```php
230233 $executor = new TimeoutExecutor(
231 new UdpTransportExecutor($loop),
234 new UdpTransportExecutor($nameserver, $loop),
232235 3.0,
233236 $loop
234237 );
241244 ```php
242245 $executor = new RetryExecutor(
243246 new TimeoutExecutor(
244 new UdpTransportExecutor($loop),
247 new UdpTransportExecutor($nameserver, $loop),
245248 3.0,
246249 $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 )
247270 )
248271 );
249272 ```
254277 packages. Higher-level components should take advantage of the Datagram
255278 component instead of reimplementing this socket logic from scratch.
256279
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
257401 ### HostsFileExecutor
258402
259403 Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
263407 ```php
264408 $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
265409
266 $executor = new UdpTransportExecutor($loop);
410 $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
267411 $executor = new HostsFileExecutor($hosts, $executor);
268412
269413 $executor->query(
270 '8.8.8.8:53',
271414 new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
272415 );
273416 ```
277420 The recommended way to install this library is [through Composer](https://getcomposer.org).
278421 [New to Composer?](https://getcomposer.org/doc/00-intro.md)
279422
423 This project follows [SemVer](https://semver.org/).
280424 This will install the latest supported version:
281425
282426 ```bash
283 $ composer require react/dns:^0.4.16
427 $ composer require react/dns:^1.2
284428 ```
285429
286430 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
44 "license": "MIT",
55 "require": {
66 "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"
1211 },
1312 "require-dev": {
1413 "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"
1615 },
1716 "autoload": {
1817 "psr-4": { "React\\Dns\\": "src" }
00 <?php
1
2 // $ php examples/12-all-types.php
3 // $ php examples/12-all-types.php myserverplace.de SSHFP
14
25 use React\Dns\Config\Config;
36 use React\Dns\Resolver\Factory;
77 require __DIR__ . '/../vendor/autoload.php';
88
99 $loop = Factory::create();
10 $executor = new UdpTransportExecutor($loop);
10 $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
1111
1212 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
1313
1414 $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN);
1515 $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN);
1616
17 $executor->query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) {
17 $executor->query($ipv4Query)->then(function (Message $message) {
1818 foreach ($message->answers as $answer) {
1919 echo 'IPv4: ' . $answer->data . PHP_EOL;
2020 }
2121 }, 'printf');
22 $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) {
22 $executor->query($ipv6Query)->then(function (Message $message) {
2323 foreach ($message->answers as $answer) {
2424 echo 'IPv6: ' . $answer->data . PHP_EOL;
2525 }
00 <?php
1
2 // $ php examples/92-query-any.php mailbox.org
3 // $ php examples/92-query-any.php _carddav._tcp.mailbox.org
14
25 use React\Dns\Model\Message;
36 use React\Dns\Model\Record;
47 use React\Dns\Query\Query;
5 use React\Dns\Query\UdpTransportExecutor;
8 use React\Dns\Query\TcpTransportExecutor;
69 use React\EventLoop\Factory;
710
811 require __DIR__ . '/../vendor/autoload.php';
912
1013 $loop = Factory::create();
11 $executor = new UdpTransportExecutor($loop);
14 $executor = new TcpTransportExecutor('8.8.8.8:53', $loop);
1215
1316 $name = isset($argv[1]) ? $argv[1] : 'google.com';
1417
1518 $any = new Query($name, Message::TYPE_ANY, Message::CLASS_IN);
1619
17 $executor->query('8.8.8.8:53', $any)->then(function (Message $message) {
20 $executor->query($any)->then(function (Message $message) {
1821 foreach ($message->answers as $answer) {
1922 /* @var $answer Record */
2023
4851 $data = implode(' ', $data);
4952 break;
5053 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
5255 $type = 'SRV';
5356 $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);
5462 break;
5563 case Message::TYPE_SOA:
5664 // SOA records contain structured data, dump structure here
5765 $type = 'SOA';
5866 $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'] . '"';
5972 break;
6073 default:
6174 // unknown type uses HEX format
77 convertWarningsToExceptions="true"
88 processIsolation="false"
99 stopOnFailure="false"
10 syntaxCheck="false"
1110 bootstrap="vendor/autoload.php"
1211 >
1312 <testsuites>
11
22 namespace React\Dns;
33
4 class BadServerException extends \Exception
4 final class BadServerException extends \Exception
55 {
66 }
33
44 use RuntimeException;
55
6 class Config
6 final class Config
77 {
88 /**
99 * Loads the system DNS configuration
+0
-73
src/Config/FilesystemFactory.php less more
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 }
7171
7272 return new self($contents);
7373 }
74
75 private $contents;
7476
7577 /**
7678 * Instantiate new hosts file with the given hosts file contents
+0
-59
src/Model/HeaderBag.php less more
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 }
33
44 use React\Dns\Query\Query;
55
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
712 {
813 const TYPE_A = 1;
914 const TYPE_NS = 2;
1419 const TYPE_TXT = 16;
1520 const TYPE_AAAA = 28;
1621 const TYPE_SRV = 33;
22 const TYPE_SSHFP = 44;
1723 const TYPE_ANY = 255;
24 const TYPE_CAA = 257;
1825
1926 const CLASS_IN = 1;
2027
3845 public static function createRequestForQuery(Query $query)
3946 {
4047 $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;
4551
4652 return $request;
4753 }
5662 public static function createResponseWithAnswersForQuery(Query $query, array $answers)
5763 {
5864 $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;
6468
65 $response->questions[] = (array) $query;
69 $response->questions[] = $query;
6670
6771 foreach ($answers as $record) {
6872 $response->answers[] = $record;
6973 }
70
71 $response->prepare();
7274
7375 return $response;
7476 }
99101 return mt_rand(0, 0xffff);
100102 }
101103
102 public $header;
103 public $questions = array();
104 public $answers = array();
105 public $authority = array();
106 public $additional = array();
107
108104 /**
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
125106 *
126107 * The response message ID has to match the request message ID. This allows
127108 * the receiver to verify this is the correct response message. An outside
128109 * attacker may try to inject fake responses by "guessing" the message ID,
129110 * so this should use a proper CSPRNG to avoid possible cache poisoning.
130111 *
131 * @return int
112 * @var int 16 bit message ID
132113 * @see self::generateId()
133114 */
134 public function getId()
135 {
136 return $this->header->get('id');
137 }
115 public $id = 0;
138116
139117 /**
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 /**
141129 *
142 * @return int see self::RCODE_* constants
130 * @var bool Authoritative Answer
143131 */
144 public function getResponseCode()
145 {
146 return $this->header->get('rcode');
147 }
132 public $aa = false;
148133
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();
153186 }
11
22 namespace React\Dns\Model;
33
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
514 {
615 /**
716 * @var string hostname without trailing dot, for example "reactphp.org"
3342 *
3443 * - A:
3544 * IPv4 address string, for example "192.168.1.1".
45 *
3646 * - AAAA:
3747 * IPv6 address string, for example "::1".
48 *
3849 * - CNAME / PTR / NS:
3950 * The hostname without trailing dot, for example "reactphp.org".
51 *
4052 * - TXT:
4153 * List of string values, for example `["v=spf1 include:example.com"]`.
4254 * This is commonly a list with only a single string value, but this
4860 * suggests using key-value pairs such as `["name=test","version=1"]`, but
4961 * interpretation of this is not enforced and left up to consumers of this
5062 * library (used for DNS-SD/Zeroconf and others).
63 *
5164 * - MX:
5265 * Mail server priority (UINT16) and target hostname without trailing dot,
5366 * for example `{"priority":10,"target":"mx.example.com"}`.
5669 * referred to as exchange). If a response message contains multiple
5770 * records of this type, targets should be sorted by priority (lowest
5871 * first) - this is left up to consumers of this library (used for SMTP).
72 *
5973 * - SRV:
6074 * Service priority (UINT16), service weight (UINT16), service port (UINT16)
6175 * and target hostname without trailing dot, for example
6882 * randomly according to their weight - this is left up to consumers of
6983 * this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782)
7084 * 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 *
7193 * - SOA:
7294 * Includes master hostname without trailing dot, responsible person email
7395 * as hostname without trailing dot and serial, refresh, retry, expire and
7496 * minimum times in seconds (UINT32 each), for example:
7597 * `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial":
7698 * 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 *
77104 * - Any other unknown type:
78105 * An opaque binary string containing the RDATA as transported in the DNS
79106 * record. For forwards compatibility, you should not rely on this format
86113 */
87114 public $data;
88115
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)
90124 {
91125 $this->name = $name;
92126 $this->type = $type;
22 namespace React\Dns\Protocol;
33
44 use React\Dns\Model\Message;
5 use React\Dns\Model\HeaderBag;
5 use React\Dns\Model\Record;
6 use React\Dns\Query\Query;
67
7 class BinaryDumper
8 final class BinaryDumper
89 {
10 /**
11 * @param Message $message
12 * @return string
13 */
914 public function toBinary(Message $message)
1015 {
1116 $data = '';
1217
13 $data .= $this->headerToBinary($message->header);
18 $data .= $this->headerToBinary($message);
1419 $data .= $this->questionToBinary($message->questions);
20 $data .= $this->recordsToBinary($message->answers);
21 $data .= $this->recordsToBinary($message->authority);
22 $data .= $this->recordsToBinary($message->additional);
1523
1624 return $data;
1725 }
1826
19 private function headerToBinary(HeaderBag $header)
27 /**
28 * @param Message $message
29 * @return string
30 */
31 private function headerToBinary(Message $message)
2032 {
2133 $data = '';
2234
23 $data .= pack('n', $header->get('id'));
35 $data .= pack('n', $message->id);
2436
2537 $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;
3446
3547 $data .= pack('n', $flags);
3648
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));
4153
4254 return $data;
4355 }
4456
57 /**
58 * @param Query[] $questions
59 * @return string
60 */
4561 private function questionToBinary(array $questions)
4662 {
4763 $data = '';
4864
4965 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);
5768 }
5869
5970 return $data;
6071 }
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 }
61188 }
33
44 use React\Dns\Model\Message;
55 use React\Dns\Model\Record;
6 use React\Dns\Query\Query;
67 use InvalidArgumentException;
78
89 /**
1011 *
1112 * Obsolete and uncommon types and classes are not implemented.
1213 */
13 class Parser
14 final class Parser
1415 {
1516 /**
1617 * Parses the given raw binary message into a Message object
2122 */
2223 public function parseMessage($data)
2324 {
25 // create empty message with two additional, temporary properties for parser
2426 $message = new Message();
27 $message->data = $data;
28 $message->consumed = null;
29
2530 if ($this->parse($data, $message) !== $message) {
2631 throw new InvalidArgumentException('Unable to parse binary message');
2732 }
2833
34 unset($message->data, $message->consumed);
35
2936 return $message;
3037 }
3138
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
4139 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)
6740 {
6841 if (!isset($message->data[12 - 1])) {
6942 return;
7043 }
7144
72 $header = substr($message->data, 0, 12);
45 list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($message->data, 0, 12)));
7346 $message->consumed += 12;
7447
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 }
9295 }
9396
9497 return $message;
9598 }
9699
97 public function parseQuestion(Message $message)
100 /**
101 * @param Message $message
102 * @return ?Query
103 */
104 private function parseQuestion(Message $message)
98105 {
99106 $consumed = $message->consumed;
100107
109116
110117 $message->consumed = $consumed;
111118
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
116123 );
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)
126131 {
127132 $consumed = $message->consumed;
128133
129134 list($name, $consumed) = $this->readDomain($message->data, $consumed);
130135
131136 if ($name === null || !isset($message->data[$consumed + 10 - 1])) {
132 return;
137 return null;
133138 }
134139
135140 list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
147152 $consumed += 2;
148153
149154 if (!isset($message->data[$consumed + $rdLength - 1])) {
150 return;
155 return null;
151156 }
152157
153158 $rdata = null;
192197 'weight' => $weight,
193198 'port' => $port,
194199 '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
195212 );
196213 }
197214 } elseif (Message::TYPE_SOA === $type) {
212229 'minimum' => $minimum
213230 );
214231 }
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 }
215248 } else {
216249 // unknown types simply parse rdata as an opaque binary string
217250 $rdata = substr($message->data, $consumed, $rdLength);
220253
221254 // ensure parsing record data consumes expact number of bytes indicated in record length
222255 if ($consumed !== $expected || $rdata === null) {
223 return;
256 return null;
224257 }
225258
226259 $message->consumed = $consumed;
227260
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);
237262 }
238263
239264 private function readDomain($data, $consumed)
244269 return array(null, null);
245270 }
246271
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 );
248285 }
249286
250287 private function readLabels($data, $consumed)
293330
294331 return array($labels, $consumed);
295332 }
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 }
351333 }
+0
-55
src/Query/CachedExecutor.php less more
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 }
11
22 namespace React\Dns\Query;
33
4 class CancellationException extends \RuntimeException
4 final class CancellationException extends \RuntimeException
55 {
66 }
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
-160
src/Query/Executor.php less more
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 }
33
44 interface ExecutorInterface
55 {
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);
742 }
77 use React\Promise;
88
99 /**
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
1111 *
1212 * If the host is found in the hosts file, it will not be passed to the actual
1313 * DNS executor. If the host is not found in the hosts file, it will be passed
1414 * to the DNS executor as a fallback.
1515 */
16 class HostsFileExecutor implements ExecutorInterface
16 final class HostsFileExecutor implements ExecutorInterface
1717 {
1818 private $hosts;
1919 private $fallback;
2424 $this->fallback = $fallback;
2525 }
2626
27 public function query($nameserver, Query $query)
27 public function query(Query $query)
2828 {
2929 if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
3030 // forward lookup for type A or AAAA
6060 }
6161 }
6262
63 return $this->fallback->query($nameserver, $query);
63 return $this->fallback->query($query);
6464 }
6565
6666 private function getIpFromHost($host)
11
22 namespace React\Dns\Query;
33
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
514 {
15 /**
16 * @var string query name, i.e. hostname to look up
17 */
618 public $name;
19
20 /**
21 * @var int query type (aka QTYPE), see Message::TYPE_* constants
22 */
723 public $type;
24
25 /**
26 * @var int query class (aka QCLASS), see Message::CLASS_IN constant
27 */
828 public $class;
929
1030 /**
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
1234 */
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)
2236 {
23 if($currentTime === null) {
24 $currentTime = time();
25 }
26
2737 $this->name = $name;
2838 $this->type = $type;
2939 $this->class = $class;
30 $this->currentTime = $currentTime;
3140 }
3241 }
+0
-26
src/Query/RecordBag.php less more
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
-123
src/Query/RecordCache.php less more
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 }
44 use React\Promise\CancellablePromiseInterface;
55 use React\Promise\Deferred;
66
7 class RetryExecutor implements ExecutorInterface
7 final class RetryExecutor implements ExecutorInterface
88 {
99 private $executor;
1010 private $retries;
1515 $this->retries = $retries;
1616 }
1717
18 public function query($nameserver, Query $query)
18 public function query(Query $query)
1919 {
20 return $this->tryQuery($nameserver, $query, $this->retries);
20 return $this->tryQuery($query, $this->retries);
2121 }
2222
23 public function tryQuery($nameserver, Query $query, $retries)
23 public function tryQuery(Query $query, $retries)
2424 {
2525 $deferred = new Deferred(function () use (&$promise) {
2626 if ($promise instanceof CancellablePromiseInterface) {
3434 };
3535
3636 $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) {
3838 if (!$e instanceof TimeoutException) {
3939 $errorback = null;
4040 $deferred->reject($e);
6161 $r->setValue($e, $trace);
6262 } else {
6363 --$retries;
64 $promise = $executor->query($nameserver, $query)->then(
64 $promise = $executor->query($query)->then(
6565 $success,
6666 $errorback
6767 );
6868 }
6969 };
7070
71 $promise = $this->executor->query($nameserver, $query)->then(
71 $promise = $this->executor->query($query)->then(
7272 $success,
7373 $errorback
7474 );
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 }
11
22 namespace React\Dns\Query;
33
4 class TimeoutException extends \Exception
4 final class TimeoutException extends \Exception
55 {
66 }
66 use React\Promise\CancellablePromiseInterface;
77 use React\Promise\Timer;
88
9 class TimeoutExecutor implements ExecutorInterface
9 final class TimeoutExecutor implements ExecutorInterface
1010 {
1111 private $executor;
1212 private $loop;
1919 $this->timeout = $timeout;
2020 }
2121
22 public function query($nameserver, Query $query)
22 public function query(Query $query)
2323 {
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) {
2525 if ($e instanceof Timer\TimeoutException) {
2626 $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e);
2727 }
1818 *
1919 * ```php
2020 * $loop = Factory::create();
21 * $executor = new UdpTransportExecutor($loop);
21 * $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
2222 *
2323 * $executor->query(
24 * '8.8.8.8:53',
2524 * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
2625 * )->then(function (Message $message) {
2726 * foreach ($message->answers as $answer) {
3938 *
4039 * ```php
4140 * $executor = new TimeoutExecutor(
42 * new UdpTransportExecutor($loop),
41 * new UdpTransportExecutor($nameserver, $loop),
4342 * 3.0,
4443 * $loop
4544 * );
5251 * ```php
5352 * $executor = new RetryExecutor(
5453 * new TimeoutExecutor(
55 * new UdpTransportExecutor($loop),
54 * new UdpTransportExecutor($nameserver, $loop),
5655 * 3.0,
5756 * $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 * )
5877 * )
5978 * );
6079 * ```
6584 * packages. Higher-level components should take advantage of the Datagram
6685 * component instead of reimplementing this socket logic from scratch.
6786 */
68 class UdpTransportExecutor implements ExecutorInterface
87 final class UdpTransportExecutor implements ExecutorInterface
6988 {
89 private $nameserver;
7090 private $loop;
7191 private $parser;
7292 private $dumper;
7393
7494 /**
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
7897 */
79 public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null)
98 public function __construct($nameserver, LoopInterface $loop)
8099 {
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 . ']';
86103 }
87104
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);
88111 $this->loop = $loop;
89 $this->parser = $parser;
90 $this->dumper = $dumper;
112 $this->parser = new Parser();
113 $this->dumper = new BinaryDumper();
91114 }
92115
93 public function query($nameserver, Query $query)
116 public function query(Query $query)
94117 {
95118 $request = Message::createRequestForQuery($query);
96119
97120 $queryData = $this->dumper->toBinary($request);
98121 if (isset($queryData[512])) {
99122 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
101125 ));
102126 }
103127
104128 // 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);
106130 if ($socket === false) {
107131 return \React\Promise\reject(new \RuntimeException(
108132 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')',
139163
140164 // ignore and await next if we received an unexpected response ID
141165 // 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) {
143167 return;
144168 }
145169
147171 $loop->removeReadStream($socket);
148172 \fclose($socket);
149173
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 ));
152179 return;
153180 }
154181
11
22 namespace React\Dns;
33
4 class RecordNotFoundException extends \Exception
4 final class RecordNotFoundException extends \Exception
55 {
66 }
44 use React\Cache\ArrayCache;
55 use React\Cache\CacheInterface;
66 use React\Dns\Config\HostsFile;
7 use React\Dns\Query\CachedExecutor;
7 use React\Dns\Query\CachingExecutor;
8 use React\Dns\Query\CoopExecutor;
89 use React\Dns\Query\ExecutorInterface;
910 use React\Dns\Query\HostsFileExecutor;
10 use React\Dns\Query\RecordCache;
1111 use React\Dns\Query\RetryExecutor;
12 use React\Dns\Query\SelectiveTransportExecutor;
13 use React\Dns\Query\TcpTransportExecutor;
1214 use React\Dns\Query\TimeoutExecutor;
1315 use React\Dns\Query\UdpTransportExecutor;
1416 use React\EventLoop\LoopInterface;
1517
16 class Factory
18 final class Factory
1719 {
20 /**
21 * @param string $nameserver
22 * @param LoopInterface $loop
23 * @return \React\Dns\Resolver\ResolverInterface
24 */
1825 public function create($nameserver, LoopInterface $loop)
1926 {
20 $nameserver = $this->addPortToServerIfMissing($nameserver);
21 $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop));
27 $executor = $this->decorateHostsFileExecutor($this->createExecutor($nameserver, $loop));
2228
23 return new Resolver($nameserver, $executor);
29 return new Resolver($executor);
2430 }
2531
32 /**
33 * @param string $nameserver
34 * @param LoopInterface $loop
35 * @param ?CacheInterface $cache
36 * @return \React\Dns\Resolver\ResolverInterface
37 */
2638 public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null)
2739 {
40 // default to keeping maximum of 256 responses in cache unless explicitly given
2841 if (!($cache instanceof CacheInterface)) {
29 $cache = new ArrayCache();
42 $cache = new ArrayCache(256);
3043 }
3144
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);
3448
35 return new Resolver($nameserver, $executor);
49 return new Resolver($executor);
3650 }
3751
3852 /**
6579 return $executor;
6680 }
6781
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)
69101 {
70102 return new TimeoutExecutor(
71 new UdpTransportExecutor($loop),
103 new TcpTransportExecutor($nameserver, $loop),
72104 5.0,
73105 $loop
74106 );
75107 }
76108
77 protected function createRetryExecutor(LoopInterface $loop)
109 private function createUdpExecutor($nameserver, LoopInterface $loop)
78110 {
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 );
99121 }
100122 }
55 use React\Dns\Query\ExecutorInterface;
66 use React\Dns\Query\Query;
77 use React\Dns\RecordNotFoundException;
8 use React\Promise\PromiseInterface;
98
10 class Resolver
9 /**
10 * @see ResolverInterface for the base interface
11 */
12 final class Resolver implements ResolverInterface
1113 {
12 private $nameserver;
1314 private $executor;
1415
15 public function __construct($nameserver, ExecutorInterface $executor)
16 public function __construct(ExecutorInterface $executor)
1617 {
17 $this->nameserver = $nameserver;
1818 $this->executor = $executor;
1919 }
2020
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 */
5921 public function resolve($domain)
6022 {
6123 return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
6325 });
6426 }
6527
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 */
11228 public function resolveAll($domain, $type)
11329 {
11430 $query = new Query($domain, $type, Message::CLASS_IN);
11531 $that = $this;
11632
11733 return $this->executor->query(
118 $this->nameserver,
11934 $query
12035 )->then(function (Message $response) use ($query, $that) {
12136 return $that->extractValues($query, $response);
12237 });
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)];
13338 }
13439
13540 /**
14449 public function extractValues(Query $query, Message $response)
14550 {
14651 // reject if response code indicates this is an error response message
147 $code = $response->getResponseCode();
52 $code = $response->rcode;
14853 if ($code !== Message::RCODE_OK) {
14954 switch ($code) {
15055 case Message::RCODE_FORMAT_ERROR:
18287 }
18388
18489 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);
19390 }
19491
19592 /**
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 }
9898 public function testLoadsFromWmicOnWindows()
9999 {
100100 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;
102106 }
103107
104108 $config = Config::loadWmicBlocking();
+0
-70
tests/Config/FilesystemFactoryTest.php less more
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 }
4646 /**
4747 * @group internet
4848 */
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 */
4977 public function testResolveAllGoogleMxResolvesWithCache()
5078 {
5179 $factory = new Factory();
5280 $this->resolver = $factory->createCached('8.8.8.8', $this->loop);
5381
5482 $promise = $this->resolver->resolveAll('google.com', Message::TYPE_MX);
83 $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever());
84
85