├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
└── src
├── BadServerException.php
├── Config
├── Config.php
└── HostsFile.php
├── Model
├── Message.php
└── Record.php
├── Protocol
├── BinaryDumper.php
└── Parser.php
├── Query
├── CachingExecutor.php
├── CancellationException.php
├── CoopExecutor.php
├── ExecutorInterface.php
├── FallbackExecutor.php
├── HostsFileExecutor.php
├── Query.php
├── RetryExecutor.php
├── SelectiveTransportExecutor.php
├── TcpTransportExecutor.php
├── TimeoutException.php
├── TimeoutExecutor.php
└── UdpTransportExecutor.php
├── RecordNotFoundException.php
└── Resolver
├── Factory.php
├── Resolver.php
└── ResolverInterface.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 1.12.0 (2023-11-29)
4 |
5 | * Feature: Full PHP 8.3 compatibility.
6 | (#217 by @sergiy-petrov)
7 |
8 | * Update test environment and avoid unhandled promise rejections.
9 | (#215, #216 and #218 by @clue)
10 |
11 | ## 1.11.0 (2023-06-02)
12 |
13 | * Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
14 | (#213 by @clue)
15 |
16 | * Improve test suite and project setup and report failed assertions.
17 | (#210 by @clue, #212 by @WyriHaximus and #209 and #211 by @SimonFrings)
18 |
19 | ## 1.10.0 (2022-09-08)
20 |
21 | * Feature: Full support for PHP 8.2 release.
22 | (#201 by @clue and #207 by @WyriHaximus)
23 |
24 | * Feature: Optimize forward compatibility with Promise v3, avoid hitting autoloader.
25 | (#202 by @clue)
26 |
27 | * Feature / Fix: Improve error reporting when custom error handler is used.
28 | (#197 by @clue)
29 |
30 | * Fix: Fix invalid references in exception stack trace.
31 | (#191 by @clue)
32 |
33 | * Minor documentation improvements.
34 | (#195 by @SimonFrings and #203 by @nhedger)
35 |
36 | * Improve test suite, update to use default loop and new reactphp/async package.
37 | (#204, #205 and #206 by @clue and #196 by @SimonFrings)
38 |
39 | ## 1.9.0 (2021-12-20)
40 |
41 | * Feature: Full support for PHP 8.1 release and prepare PHP 8.2 compatibility
42 | by refactoring `Parser` to avoid assigning dynamic properties.
43 | (#188 and #186 by @clue and #184 by @SimonFrings)
44 |
45 | * Feature: Avoid dependency on `ext-filter`.
46 | (#185 by @clue)
47 |
48 | * Feature / Fix: Skip invalid nameserver entries from `resolv.conf` and ignore IPv6 zone IDs.
49 | (#187 by @clue)
50 |
51 | * Feature / Fix: Reduce socket read chunk size for queries over TCP/IP.
52 | (#189 by @clue)
53 |
54 | ## 1.8.0 (2021-07-11)
55 |
56 | A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
57 |
58 | * Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
59 | (#182 by @clue)
60 |
61 | ```php
62 | // old (still supported)
63 | $factory = new React\Dns\Resolver\Factory();
64 | $resolver = $factory->create($config, $loop);
65 |
66 | // new (using default loop)
67 | $factory = new React\Dns\Resolver\Factory();
68 | $resolver = $factory->create($config);
69 | ```
70 |
71 | ## 1.7.0 (2021-06-25)
72 |
73 | * Feature: Update DNS `Factory` to accept complete `Config` object.
74 | Add new `FallbackExecutor` and use fallback DNS servers when `Config` lists multiple servers.
75 | (#179 and #180 by @clue)
76 |
77 | ```php
78 | // old (still supported)
79 | $config = React\Dns\Config\Config::loadSystemConfigBlocking();
80 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
81 | $resolver = $factory->create($server, $loop);
82 |
83 | // new
84 | $config = React\Dns\Config\Config::loadSystemConfigBlocking();
85 | if (!$config->nameservers) {
86 | $config->nameservers[] = '8.8.8.8';
87 | }
88 | $resolver = $factory->create($config, $loop);
89 | ```
90 |
91 | ## 1.6.0 (2021-06-21)
92 |
93 | * Feature: Add support for legacy `SPF` record type.
94 | (#178 by @akondas and @clue)
95 |
96 | * Fix: Fix integer overflow for TCP/IP chunk size on 32 bit platforms.
97 | (#177 by @clue)
98 |
99 | ## 1.5.0 (2021-03-05)
100 |
101 | * Feature: Improve error reporting when query fails, include domain and query type and DNS server address where applicable.
102 | (#174 by @clue)
103 |
104 | * Feature: Improve error handling when sending data to DNS server fails (macOS).
105 | (#171 and #172 by @clue)
106 |
107 | * Fix: Improve DNS response parser to limit recursion for compressed labels.
108 | (#169 by @clue)
109 |
110 | * Improve test suite, use GitHub actions for continuous integration (CI).
111 | (#170 by @SimonFrings)
112 |
113 | ## 1.4.0 (2020-09-18)
114 |
115 | * Feature: Support upcoming PHP 8.
116 | (#168 by @clue)
117 |
118 | * Improve test suite and update to PHPUnit 9.3.
119 | (#164 by @clue, #165 and #166 by @SimonFrings and #167 by @WyriHaximus)
120 |
121 | ## 1.3.0 (2020-07-10)
122 |
123 | * Feature: Forward compatibility with react/promise v3.
124 | (#153 by @WyriHaximus)
125 |
126 | * Feature: Support parsing `OPT` records (EDNS0).
127 | (#157 by @clue)
128 |
129 | * Fix: Avoid PHP warnings due to lack of args in exception trace on PHP 7.4.
130 | (#160 by @clue)
131 |
132 | * Improve test suite and add `.gitattributes` to exclude dev files from exports.
133 | Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite.
134 | (#154 by @reedy, #156 by @clue and #163 by @SimonFrings)
135 |
136 | ## 1.2.0 (2019-08-15)
137 |
138 | * Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection,
139 | add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and
140 | automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`.
141 | (#145, #146, #147 and #148 by @clue)
142 |
143 | * Feature: Support escaping literal dots and special characters in domain names.
144 | (#144 by @clue)
145 |
146 | ## 1.1.0 (2019-07-18)
147 |
148 | * Feature: Support parsing `CAA` and `SSHFP` records.
149 | (#141 and #142 by @clue)
150 |
151 | * Feature: Add `ResolverInterface` as common interface for `Resolver` class.
152 | (#139 by @clue)
153 |
154 | * Fix: Add missing private property definitions and
155 | remove unneeded dependency on `react/stream`.
156 | (#140 and #143 by @clue)
157 |
158 | ## 1.0.0 (2019-07-11)
159 |
160 | * First stable LTS release, now following [SemVer](https://semver.org/).
161 | We'd like to emphasize that this component is production ready and battle-tested.
162 | We plan to support all long-term support (LTS) releases for at least 24 months,
163 | so you have a rock-solid foundation to build on top of.
164 |
165 | This update involves a number of BC breaks due to dropped support for
166 | deprecated functionality and some internal API cleanup. We've tried hard to
167 | avoid BC breaks where possible and minimize impact otherwise. We expect that
168 | most consumers of this package will actually not be affected by any BC
169 | breaks, see below for more details:
170 |
171 | * BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions
172 | instead of nested arrays and increase code coverage to 100%.
173 | (#130 by @clue)
174 |
175 | * BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`,
176 | remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and
177 | add API documentation for `ExecutorInterface`.
178 | (#135, #137 and #138 by @clue)
179 |
180 | * BC break: Replace `HeaderBag` attributes with simple `Message` properties.
181 | (#132 by @clue)
182 |
183 | * BC break: Mark all `Record` attributes as required, add documentation vs `Query`.
184 | (#136 by @clue)
185 |
186 | * BC break: Mark all classes as final to discourage inheritance
187 | (#134 by @WyriHaximus)
188 |
189 | ## 0.4.19 (2019-07-10)
190 |
191 | * Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6.
192 | (#133 by @clue)
193 |
194 | ## 0.4.18 (2019-09-07)
195 |
196 | * Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`,
197 | respect TTL from response records when caching and do not cache truncated responses.
198 | (#129 by @clue)
199 |
200 | * Feature: Limit cache size to 256 last responses by default.
201 | (#127 by @clue)
202 |
203 | * Feature: Cooperatively resolve hosts to avoid running same query concurrently.
204 | (#125 by @clue)
205 |
206 | ## 0.4.17 (2019-04-01)
207 |
208 | * Feature: Support parsing `authority` and `additional` records from DNS response.
209 | (#123 by @clue)
210 |
211 | * Feature: Support dumping records as part of outgoing binary DNS message.
212 | (#124 by @clue)
213 |
214 | * Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0
215 | (#121 by @clue)
216 |
217 | * Improve test suite to add forward compatibility with PHPUnit 7,
218 | test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM.
219 | (#122 by @clue)
220 |
221 | ## 0.4.16 (2018-11-11)
222 |
223 | * Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references.
224 | (#118 by @clue)
225 |
226 | * Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages,
227 | malformed record data or malformed compressed domain name labels.
228 | (#115 and #117 by @clue)
229 |
230 | * Fix: Fix interpretation of TTL as UINT32 with most significant bit unset.
231 | (#116 by @clue)
232 |
233 | * Fix: Fix caching advanced MX/SRV/TXT/SOA structures.
234 | (#112 by @clue)
235 |
236 | ## 0.4.15 (2018-07-02)
237 |
238 | * Feature: Add `resolveAll()` method to support custom query types in `Resolver`.
239 | (#110 by @clue and @WyriHaximus)
240 |
241 | ```php
242 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
243 | echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
244 | });
245 | ```
246 |
247 | * Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
248 | (#104, #105, #106, #107 and #108 by @clue)
249 |
250 | * Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data.
251 | (#104 by @clue)
252 |
253 | * Feature: Improve error messages for failed queries and improve documentation.
254 | (#109 by @clue)
255 |
256 | * Feature: Add reverse DNS lookup example.
257 | (#111 by @clue)
258 |
259 | ## 0.4.14 (2018-06-26)
260 |
261 | * Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages
262 | to avoid cache poisoning attacks and deprecate legacy `Executor`.
263 | (#101 and #103 by @clue)
264 |
265 | * Feature: Forward compatibility with Cache 0.5
266 | (#102 by @clue)
267 |
268 | * Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API.
269 | (#99 by @clue)
270 |
271 | ## 0.4.13 (2018-02-27)
272 |
273 | * Add `Config::loadSystemConfigBlocking()` to load default system config
274 | and support parsing DNS config on all supported platforms
275 | (`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows)
276 | (#92, #93, #94 and #95 by @clue)
277 |
278 | ```php
279 | $config = Config::loadSystemConfigBlocking();
280 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
281 | ```
282 |
283 | * Remove unneeded cyclic dependency on react/socket
284 | (#96 by @clue)
285 |
286 | ## 0.4.12 (2018-01-14)
287 |
288 | * Improve test suite by adding forward compatibility with PHPUnit 6,
289 | test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases,
290 | add test group to skip integration tests relying on internet connection
291 | and add minor documentation improvements.
292 | (#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor)
293 |
294 | ## 0.4.11 (2017-08-25)
295 |
296 | * Feature: Support resolving from default hosts file
297 | (#75, #76 and #77 by @clue)
298 |
299 | This means that resolving hosts such as `localhost` will now work as
300 | expected across all platforms with no changes required:
301 |
302 | ```php
303 | $resolver->resolve('localhost')->then(function ($ip) {
304 | echo 'IP: ' . $ip;
305 | });
306 | ```
307 |
308 | The new `HostsExecutor` exists for advanced usage and is otherwise used
309 | internally for this feature.
310 |
311 | ## 0.4.10 (2017-08-10)
312 |
313 | * Feature: Forward compatibility with EventLoop v1.0 and v0.5 and
314 | lock minimum dependencies and work around circular dependency for tests
315 | (#70 and #71 by @clue)
316 |
317 | * Fix: Work around DNS timeout issues for Windows users
318 | (#74 by @clue)
319 |
320 | * Documentation and examples for advanced usage
321 | (#66 by @WyriHaximus)
322 |
323 | * Remove broken TCP code, do not retry with invalid TCP query
324 | (#73 by @clue)
325 |
326 | * Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and
327 | lock Travis distro so new defaults will not break the build and
328 | fix failing tests for PHP 7.1
329 | (#68 by @WyriHaximus and #69 and #72 by @clue)
330 |
331 | ## 0.4.9 (2017-05-01)
332 |
333 | * Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
334 | (#61 by @clue)
335 |
336 | ## 0.4.8 (2017-04-16)
337 |
338 | * Feature: Add support for the AAAA record type to the protocol parser
339 | (#58 by @othillo)
340 |
341 | * Feature: Add support for the PTR record type to the protocol parser
342 | (#59 by @othillo)
343 |
344 | ## 0.4.7 (2017-03-31)
345 |
346 | * Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component
347 | (#57 by @clue)
348 |
349 | ## 0.4.6 (2017-03-11)
350 |
351 | * Fix: Fix DNS timeout issues for Windows users and add forward compatibility
352 | with Stream v0.5 and upcoming v0.6
353 | (#53 by @clue)
354 |
355 | * Improve test suite by adding PHPUnit to `require-dev`
356 | (#54 by @clue)
357 |
358 | ## 0.4.5 (2017-03-02)
359 |
360 | * Fix: Ensure we ignore the case of the answer
361 | (#51 by @WyriHaximus)
362 |
363 | * Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal
364 | code re-use for upcoming versions.
365 | (#48 and #49 by @clue)
366 |
367 | ## 0.4.4 (2017-02-13)
368 |
369 | * Fix: Fix handling connection and stream errors
370 | (#45 by @clue)
371 |
372 | * Feature: Add examples and forward compatibility with upcoming Socket v0.5 component
373 | (#46 and #47 by @clue)
374 |
375 | ## 0.4.3 (2016-07-31)
376 |
377 | * Feature: Allow for cache adapter injection (#38 by @WyriHaximus)
378 |
379 | ```php
380 | $factory = new React\Dns\Resolver\Factory();
381 |
382 | $cache = new MyCustomCacheInstance();
383 | $resolver = $factory->createCached('8.8.8.8', $loop, $cache);
384 | ```
385 |
386 | * Feature: Support Promise cancellation (#35 by @clue)
387 |
388 | ```php
389 | $promise = $resolver->resolve('reactphp.org');
390 |
391 | $promise->cancel();
392 | ```
393 |
394 | ## 0.4.2 (2016-02-24)
395 |
396 | * Repository maintenance, split off from main repo, improve test suite and documentation
397 | * First class support for PHP7 and HHVM (#34 by @clue)
398 | * Adjust compatibility to 5.3 (#30 by @clue)
399 |
400 | ## 0.4.1 (2014-04-13)
401 |
402 | * Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus)
403 |
404 | ## 0.4.0 (2014-02-02)
405 |
406 | * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
407 | * BC break: Update to React/Promise 2.0
408 | * Bug fix: Properly resolve CNAME aliases
409 | * Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
410 | * Bump React dependencies to v0.4
411 |
412 | ## 0.3.2 (2013-05-10)
413 |
414 | * Feature: Support default port for IPv6 addresses (@clue)
415 |
416 | ## 0.3.0 (2013-04-14)
417 |
418 | * Bump React dependencies to v0.3
419 |
420 | ## 0.2.6 (2012-12-26)
421 |
422 | * Feature: New cache component, used by DNS
423 |
424 | ## 0.2.5 (2012-11-26)
425 |
426 | * Version bump
427 |
428 | ## 0.2.4 (2012-11-18)
429 |
430 | * Feature: Change to promise-based API (@jsor)
431 |
432 | ## 0.2.3 (2012-11-14)
433 |
434 | * Version bump
435 |
436 | ## 0.2.2 (2012-10-28)
437 |
438 | * Feature: DNS executor timeout handling (@arnaud-lb)
439 | * Feature: DNS retry executor (@arnaud-lb)
440 |
441 | ## 0.2.1 (2012-10-14)
442 |
443 | * Minor adjustments to DNS parser
444 |
445 | ## 0.2.0 (2012-09-10)
446 |
447 | * Feature: DNS resolver
448 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is furnished
10 | to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # DNS
2 |
3 | [](https://github.com/reactphp/dns/actions)
4 | [](https://packagist.org/packages/react/dns)
5 |
6 | Async DNS resolver for [ReactPHP](https://reactphp.org/).
7 |
8 | > **Development version:** This branch contains the code for the upcoming v3
9 | > release. For the code of the current stable v1 release, check out the
10 | > [`1.x` branch](https://github.com/reactphp/dns/tree/1.x).
11 | >
12 | > The upcoming v3 release will be the way forward for this package. However,
13 | > we will still actively support v1 for those not yet on the latest version.
14 | > See also [installation instructions](#install) for more details.
15 |
16 | The main point of the DNS component is to provide async DNS resolution.
17 | However, it is really a toolkit for working with DNS messages, and could
18 | easily be used to create a DNS server.
19 |
20 | **Table of contents**
21 |
22 | * [Basic usage](#basic-usage)
23 | * [Caching](#caching)
24 | * [Custom cache adapter](#custom-cache-adapter)
25 | * [ResolverInterface](#resolverinterface)
26 | * [resolve()](#resolve)
27 | * [resolveAll()](#resolveall)
28 | * [Advanced usage](#advanced-usage)
29 | * [UdpTransportExecutor](#udptransportexecutor)
30 | * [TcpTransportExecutor](#tcptransportexecutor)
31 | * [SelectiveTransportExecutor](#selectivetransportexecutor)
32 | * [HostsFileExecutor](#hostsfileexecutor)
33 | * [Install](#install)
34 | * [Tests](#tests)
35 | * [License](#license)
36 | * [References](#references)
37 |
38 | ## Basic usage
39 |
40 | The most basic usage is to just create a resolver through the resolver
41 | factory. All you need to give it is a nameserver, then you can start resolving
42 | names, baby!
43 |
44 | ```php
45 | $config = React\Dns\Config\Config::loadSystemConfigBlocking();
46 | if (!$config->nameservers) {
47 | $config->nameservers[] = '8.8.8.8';
48 | }
49 |
50 | $factory = new React\Dns\Resolver\Factory();
51 | $dns = $factory->create($config);
52 |
53 | $dns->resolve('igor.io')->then(function ($ip) {
54 | echo "Host: $ip\n";
55 | });
56 | ```
57 |
58 | See also the [first example](examples).
59 |
60 | The `Config` class can be used to load the system default config. This is an
61 | operation that may access the filesystem and block. Ideally, this method should
62 | thus be executed only once before the loop starts and not repeatedly while it is
63 | running.
64 | Note that this class may return an *empty* configuration if the system config
65 | can not be loaded. As such, you'll likely want to apply a default nameserver
66 | as above if none can be found.
67 |
68 | > Note that the factory loads the hosts file from the filesystem once when
69 | creating the resolver instance.
70 | Ideally, this method should thus be executed only once before the loop starts
71 | and not repeatedly while it is running.
72 |
73 | But there's more.
74 |
75 | ## Caching
76 |
77 | You can cache results by configuring the resolver to use a `CachedExecutor`:
78 |
79 | ```php
80 | $config = React\Dns\Config\Config::loadSystemConfigBlocking();
81 | if (!$config->nameservers) {
82 | $config->nameservers[] = '8.8.8.8';
83 | }
84 |
85 | $factory = new React\Dns\Resolver\Factory();
86 | $dns = $factory->createCached($config);
87 |
88 | $dns->resolve('igor.io')->then(function ($ip) {
89 | echo "Host: $ip\n";
90 | });
91 |
92 | ...
93 |
94 | $dns->resolve('igor.io')->then(function ($ip) {
95 | echo "Host: $ip\n";
96 | });
97 | ```
98 |
99 | If the first call returns before the second, only one query will be executed.
100 | The second result will be served from an in memory cache.
101 | This is particularly useful for long running scripts where the same hostnames
102 | have to be looked up multiple times.
103 |
104 | See also the [third example](examples).
105 |
106 | ### Custom cache adapter
107 |
108 | By default, the above will use an in memory cache.
109 |
110 | You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead:
111 |
112 | ```php
113 | $cache = new React\Cache\ArrayCache();
114 | $factory = new React\Dns\Resolver\Factory();
115 | $dns = $factory->createCached('8.8.8.8', null, $cache);
116 | ```
117 |
118 | See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
119 |
120 | ## ResolverInterface
121 |
122 |
123 |
124 | ### resolve()
125 |
126 | The `resolve(string $domain): PromiseInterface` method can be used to
127 | resolve the given $domain name to a single IPv4 address (type `A` query).
128 |
129 | ```php
130 | $resolver->resolve('reactphp.org')->then(function ($ip) {
131 | echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
132 | });
133 | ```
134 |
135 | This is one of the main methods in this package. It sends a DNS query
136 | for the given $domain name to your DNS server and returns a single IP
137 | address on success.
138 |
139 | If the DNS server sends a DNS response message that contains more than
140 | one IP address for this query, it will randomly pick one of the IP
141 | addresses from the response. If you want the full list of IP addresses
142 | or want to send a different type of query, you should use the
143 | [`resolveAll()`](#resolveall) method instead.
144 |
145 | If the DNS server sends a DNS response message that indicates an error
146 | code, this method will reject with a `RecordNotFoundException`. Its
147 | message and code can be used to check for the response code.
148 |
149 | If the DNS communication fails and the server does not respond with a
150 | valid response message, this message will reject with an `Exception`.
151 |
152 | Pending DNS queries can be cancelled by cancelling its pending promise like so:
153 |
154 | ```php
155 | $promise = $resolver->resolve('reactphp.org');
156 |
157 | $promise->cancel();
158 | ```
159 |
160 | ### resolveAll()
161 |
162 | The `resolveAll(string $host, int $type): PromiseInterface` method can be used to
163 | resolve all record values for the given $domain name and query $type.
164 |
165 | ```php
166 | $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
167 | echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
168 | });
169 |
170 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
171 | echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
172 | });
173 | ```
174 |
175 | This is one of the main methods in this package. It sends a DNS query
176 | for the given $domain name to your DNS server and returns a list with all
177 | record values on success.
178 |
179 | If the DNS server sends a DNS response message that contains one or more
180 | records for this query, it will return a list with all record values
181 | from the response. You can use the `Message::TYPE_*` constants to control
182 | which type of query will be sent. Note that this method always returns a
183 | list of record values, but each record value type depends on the query
184 | type. For example, it returns the IPv4 addresses for type `A` queries,
185 | the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
186 | `CNAME` and `PTR` queries and structured data for other queries. See also
187 | the `Record` documentation for more details.
188 |
189 | If the DNS server sends a DNS response message that indicates an error
190 | code, this method will reject with a `RecordNotFoundException`. Its
191 | message and code can be used to check for the response code.
192 |
193 | If the DNS communication fails and the server does not respond with a
194 | valid response message, this message will reject with an `Exception`.
195 |
196 | Pending DNS queries can be cancelled by cancelling its pending promise like so:
197 |
198 | ```php
199 | $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
200 |
201 | $promise->cancel();
202 | ```
203 |
204 | ## Advanced Usage
205 |
206 | ### UdpTransportExecutor
207 |
208 | The `UdpTransportExecutor` can be used to
209 | send DNS queries over a UDP transport.
210 |
211 | This is the main class that sends a DNS query to your DNS server and is used
212 | internally by the `Resolver` for the actual message transport.
213 |
214 | For more advanced usages one can utilize this class directly.
215 | The following example looks up the `IPv6` address for `igor.io`.
216 |
217 | ```php
218 | $executor = new UdpTransportExecutor('8.8.8.8:53');
219 |
220 | $executor->query(
221 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
222 | )->then(function (Message $message) {
223 | foreach ($message->answers as $answer) {
224 | echo 'IPv6: ' . $answer->data . PHP_EOL;
225 | }
226 | }, 'printf');
227 | ```
228 |
229 | See also the [fourth example](examples).
230 |
231 | Note that this executor does not implement a timeout, so you will very likely
232 | want to use this in combination with a `TimeoutExecutor` like this:
233 |
234 | ```php
235 | $executor = new TimeoutExecutor(
236 | new UdpTransportExecutor($nameserver),
237 | 3.0
238 | );
239 | ```
240 |
241 | Also note that this executor uses an unreliable UDP transport and that it
242 | does not implement any retry logic, so you will likely want to use this in
243 | combination with a `RetryExecutor` like this:
244 |
245 | ```php
246 | $executor = new RetryExecutor(
247 | new TimeoutExecutor(
248 | new UdpTransportExecutor($nameserver),
249 | 3.0
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),
267 | 3.0
268 | )
269 | )
270 | );
271 | ```
272 |
273 | > Internally, this class uses PHP's UDP sockets and does not take advantage
274 | of [react/datagram](https://github.com/reactphp/datagram) purely for
275 | organizational reasons to avoid a cyclic dependency between the two
276 | packages. Higher-level components should take advantage of the Datagram
277 | component instead of reimplementing this socket logic from scratch.
278 |
279 | ### TcpTransportExecutor
280 |
281 | The `TcpTransportExecutor` class can be used to
282 | send DNS queries over a TCP/IP stream transport.
283 |
284 | This is one of the main classes that send a DNS query to your DNS server.
285 |
286 | For more advanced usages one can utilize this class directly.
287 | The following example looks up the `IPv6` address for `reactphp.org`.
288 |
289 | ```php
290 | $executor = new TcpTransportExecutor('8.8.8.8:53');
291 |
292 | $executor->query(
293 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
294 | )->then(function (Message $message) {
295 | foreach ($message->answers as $answer) {
296 | echo 'IPv6: ' . $answer->data . PHP_EOL;
297 | }
298 | }, 'printf');
299 | ```
300 |
301 | See also [example #92](examples).
302 |
303 | Note that this executor does not implement a timeout, so you will very likely
304 | want to use this in combination with a `TimeoutExecutor` like this:
305 |
306 | ```php
307 | $executor = new TimeoutExecutor(
308 | new TcpTransportExecutor($nameserver),
309 | 3.0
310 | );
311 | ```
312 |
313 | Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
314 | transport, so you do not necessarily have to implement any retry logic.
315 |
316 | Note that this executor is entirely async and as such allows you to execute
317 | queries concurrently. The first query will establish a TCP/IP socket
318 | connection to the DNS server which will be kept open for a short period.
319 | Additional queries will automatically reuse this existing socket connection
320 | to the DNS server, will pipeline multiple requests over this single
321 | connection and will keep an idle connection open for a short period. The
322 | initial TCP/IP connection overhead may incur a slight delay if you only send
323 | occasional queries – when sending a larger number of concurrent queries over
324 | an existing connection, it becomes increasingly more efficient and avoids
325 | creating many concurrent sockets like the UDP-based executor. You may still
326 | want to limit the number of (concurrent) queries in your application or you
327 | may be facing rate limitations and bans on the resolver end. For many common
328 | applications, you may want to avoid sending the same query multiple times
329 | when the first one is still pending, so you will likely want to use this in
330 | combination with a `CoopExecutor` like this:
331 |
332 | ```php
333 | $executor = new CoopExecutor(
334 | new TimeoutExecutor(
335 | new TcpTransportExecutor($nameserver),
336 | 3.0
337 | )
338 | );
339 | ```
340 |
341 | > Internally, this class uses PHP's TCP/IP sockets and does not take advantage
342 | of [react/socket](https://github.com/reactphp/socket) purely for
343 | organizational reasons to avoid a cyclic dependency between the two
344 | packages. Higher-level components should take advantage of the Socket
345 | component instead of reimplementing this socket logic from scratch.
346 |
347 | ### SelectiveTransportExecutor
348 |
349 | The `SelectiveTransportExecutor` class can be used to
350 | Send DNS queries over a UDP or TCP/IP stream transport.
351 |
352 | This class will automatically choose the correct transport protocol to send
353 | a DNS query to your DNS server. It will always try to send it over the more
354 | efficient UDP transport first. If this query yields a size related issue
355 | (truncated messages), it will retry over a streaming TCP/IP transport.
356 |
357 | For more advanced usages one can utilize this class directly.
358 | The following example looks up the `IPv6` address for `reactphp.org`.
359 |
360 | ```php
361 | $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
362 |
363 | $executor->query(
364 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
365 | )->then(function (Message $message) {
366 | foreach ($message->answers as $answer) {
367 | echo 'IPv6: ' . $answer->data . PHP_EOL;
368 | }
369 | }, 'printf');
370 | ```
371 |
372 | Note that this executor only implements the logic to select the correct
373 | transport for the given DNS query. Implementing the correct transport logic,
374 | implementing timeouts and any retry logic is left up to the given executors,
375 | see also [`UdpTransportExecutor`](#udptransportexecutor) and
376 | [`TcpTransportExecutor`](#tcptransportexecutor) for more details.
377 |
378 | Note that this executor is entirely async and as such allows you to execute
379 | any number of queries concurrently. You should probably limit the number of
380 | concurrent queries in your application or you're very likely going to face
381 | rate limitations and bans on the resolver end. For many common applications,
382 | you may want to avoid sending the same query multiple times when the first
383 | one is still pending, so you will likely want to use this in combination with
384 | a `CoopExecutor` like this:
385 |
386 | ```php
387 | $executor = new CoopExecutor(
388 | new SelectiveTransportExecutor(
389 | $datagramExecutor,
390 | $streamExecutor
391 | )
392 | );
393 | ```
394 |
395 | ### HostsFileExecutor
396 |
397 | Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
398 | If you also want to take entries from your hosts file into account, you may
399 | use this code:
400 |
401 | ```php
402 | $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
403 |
404 | $executor = new UdpTransportExecutor('8.8.8.8:53');
405 | $executor = new HostsFileExecutor($hosts, $executor);
406 |
407 | $executor->query(
408 | new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
409 | );
410 | ```
411 |
412 | ## Install
413 |
414 | The recommended way to install this library is [through Composer](https://getcomposer.org/).
415 | [New to Composer?](https://getcomposer.org/doc/00-intro.md)
416 |
417 | Once released, this project will follow [SemVer](https://semver.org/).
418 | At the moment, this will install the latest development version:
419 |
420 | ```bash
421 | composer require react/dns:^3@dev
422 | ```
423 |
424 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
425 |
426 | This project aims to run on any platform and thus does not require any PHP
427 | extensions and supports running on PHP 7.1 through current PHP 8+.
428 | It's *highly recommended to use the latest supported PHP version* for this project.
429 |
430 | ## Tests
431 |
432 | To run the test suite, you first need to clone this repo and then install all
433 | dependencies [through Composer](https://getcomposer.org/):
434 |
435 | ```bash
436 | composer install
437 | ```
438 |
439 | To run the test suite, go to the project root and run:
440 |
441 | ```bash
442 | vendor/bin/phpunit
443 | ```
444 |
445 | The test suite also contains a number of functional integration tests that rely
446 | on a stable internet connection.
447 | If you do not want to run these, they can simply be skipped like this:
448 |
449 | ```bash
450 | vendor/bin/phpunit --exclude-group internet
451 | ```
452 |
453 | ## License
454 |
455 | MIT, see [LICENSE file](LICENSE).
456 |
457 | ## References
458 |
459 | * [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
460 | * [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification
461 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react/dns",
3 | "description": "Async DNS resolver for ReactPHP",
4 | "keywords": ["dns", "dns-resolver", "ReactPHP", "async"],
5 | "license": "MIT",
6 | "authors": [
7 | {
8 | "name": "Christian Lück",
9 | "homepage": "https://clue.engineering/",
10 | "email": "christian@clue.engineering"
11 | },
12 | {
13 | "name": "Cees-Jan Kiewiet",
14 | "homepage": "https://wyrihaximus.net/",
15 | "email": "reactphp@ceesjankiewiet.nl"
16 | },
17 | {
18 | "name": "Jan Sorgalla",
19 | "homepage": "https://sorgalla.com/",
20 | "email": "jsorgalla@gmail.com"
21 | },
22 | {
23 | "name": "Chris Boden",
24 | "homepage": "https://cboden.dev/",
25 | "email": "cboden@gmail.com"
26 | }
27 | ],
28 | "require": {
29 | "php": ">=7.1",
30 | "react/cache": "^1.0 || ^0.6 || ^0.5",
31 | "react/event-loop": "^1.2",
32 | "react/promise": "^3.2 || ^2.7 || ^1.2.1"
33 | },
34 | "require-dev": {
35 | "phpunit/phpunit": "^9.6 || ^7.5",
36 | "react/async": "^4.3 || ^3 || ^2",
37 | "react/promise-timer": "^1.11"
38 | },
39 | "autoload": {
40 | "psr-4": {
41 | "React\\Dns\\": "src/"
42 | }
43 | },
44 | "autoload-dev": {
45 | "psr-4": {
46 | "React\\Tests\\Dns\\": "tests/"
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/BadServerException.php:
--------------------------------------------------------------------------------
1 | `fe80:1`)
91 | if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
92 | $ip = substr($ip, 0, $pos);
93 | }
94 |
95 | if (@inet_pton($ip) !== false) {
96 | $config->nameservers[] = $ip;
97 | }
98 | }
99 |
100 | return $config;
101 | }
102 |
103 | /**
104 | * Loads the DNS configurations from Windows's WMIC (from the given command or default command)
105 | *
106 | * Note that this method blocks while loading the given command and should
107 | * thus be used with care! While this should be relatively fast for normal
108 | * WMIC commands, it remains unknown if this may block under certain
109 | * circumstances. In particular, this method should only be executed before
110 | * the loop starts, not while it is running.
111 | *
112 | * Note that this method will only try to execute the given command try to
113 | * parse its output, irrespective of whether this command exists. In
114 | * particular, this command is only available on Windows. Currently, this
115 | * will only parse valid nameserver entries from the command output and will
116 | * ignore all other output without complaining.
117 | *
118 | * Note that the previous section implies that this may return an empty
119 | * `Config` object if no valid nameserver entries can be found.
120 | *
121 | * @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
122 | * @return self
123 | * @link https://ss64.com/nt/wmic.html
124 | */
125 | public static function loadWmicBlocking($command = null)
126 | {
127 | $contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
128 | preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
129 |
130 | $config = new self();
131 | $config->nameservers = $matches[1];
132 |
133 | return $config;
134 | }
135 |
136 | public $nameservers = [];
137 | }
138 |
--------------------------------------------------------------------------------
/src/Config/HostsFile.php:
--------------------------------------------------------------------------------
1 | contents = $contents;
89 | }
90 |
91 | /**
92 | * Returns all IPs for the given hostname
93 | *
94 | * @param string $name
95 | * @return string[]
96 | */
97 | public function getIpsForHost($name)
98 | {
99 | $name = strtolower($name);
100 |
101 | $ips = [];
102 | foreach (preg_split('/\r?\n/', $this->contents) as $line) {
103 | $parts = preg_split('/\s+/', $line);
104 | $ip = array_shift($parts);
105 | if ($parts && array_search($name, $parts) !== false) {
106 | // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
107 | if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
108 | $ip = substr($ip, 0, $pos);
109 | }
110 |
111 | if (@inet_pton($ip) !== false) {
112 | $ips[] = $ip;
113 | }
114 | }
115 | }
116 |
117 | return $ips;
118 | }
119 |
120 | /**
121 | * Returns all hostnames for the given IPv4 or IPv6 address
122 | *
123 | * @param string $ip
124 | * @return string[]
125 | */
126 | public function getHostsForIp($ip)
127 | {
128 | // check binary representation of IP to avoid string case and short notation
129 | $ip = @inet_pton($ip);
130 | if ($ip === false) {
131 | return [];
132 | }
133 |
134 | $names = [];
135 | foreach (preg_split('/\r?\n/', $this->contents) as $line) {
136 | $parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
137 | $addr = (string) array_shift($parts);
138 |
139 | // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
140 | if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) {
141 | $addr = substr($addr, 0, $pos);
142 | }
143 |
144 | if (@inet_pton($addr) === $ip) {
145 | foreach ($parts as $part) {
146 | $names[] = $part;
147 | }
148 | }
149 | }
150 |
151 | return $names;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/Model/Message.php:
--------------------------------------------------------------------------------
1 | id = self::generateId();
93 | $request->rd = true;
94 | $request->questions[] = $query;
95 |
96 | return $request;
97 | }
98 |
99 | /**
100 | * Creates a new response message for the given query with the given answer records
101 | *
102 | * @param Query $query
103 | * @param Record[] $answers
104 | * @return self
105 | */
106 | public static function createResponseWithAnswersForQuery(Query $query, array $answers)
107 | {
108 | $response = new Message();
109 | $response->id = self::generateId();
110 | $response->qr = true;
111 | $response->rd = true;
112 |
113 | $response->questions[] = $query;
114 |
115 | foreach ($answers as $record) {
116 | $response->answers[] = $record;
117 | }
118 |
119 | return $response;
120 | }
121 |
122 | /**
123 | * generates a random 16 bit message ID
124 | *
125 | * This uses a CSPRNG so that an outside attacker that is sending spoofed
126 | * DNS response messages can not guess the message ID to avoid possible
127 | * cache poisoning attacks.
128 | *
129 | * @return int
130 | * @see self::getId()
131 | * @codeCoverageIgnore
132 | */
133 | private static function generateId()
134 | {
135 | return random_int(0, 0xffff);
136 | }
137 |
138 | /**
139 | * The 16 bit message ID
140 | *
141 | * The response message ID has to match the request message ID. This allows
142 | * the receiver to verify this is the correct response message. An outside
143 | * attacker may try to inject fake responses by "guessing" the message ID,
144 | * so this should use a proper CSPRNG to avoid possible cache poisoning.
145 | *
146 | * @var int 16 bit message ID
147 | * @see self::generateId()
148 | */
149 | public $id = 0;
150 |
151 | /**
152 | * @var bool Query/Response flag, query=false or response=true
153 | */
154 | public $qr = false;
155 |
156 | /**
157 | * @var int specifies the kind of query (4 bit), see self::OPCODE_* constants
158 | * @see self::OPCODE_QUERY
159 | */
160 | public $opcode = self::OPCODE_QUERY;
161 |
162 | /**
163 | *
164 | * @var bool Authoritative Answer
165 | */
166 | public $aa = false;
167 |
168 | /**
169 | * @var bool TrunCation
170 | */
171 | public $tc = false;
172 |
173 | /**
174 | * @var bool Recursion Desired
175 | */
176 | public $rd = false;
177 |
178 | /**
179 | * @var bool Recursion Available
180 | */
181 | public $ra = false;
182 |
183 | /**
184 | * @var int response code (4 bit), see self::RCODE_* constants
185 | * @see self::RCODE_OK
186 | */
187 | public $rcode = Message::RCODE_OK;
188 |
189 | /**
190 | * An array of Query objects
191 | *
192 | * ```php
193 | * $questions = [
194 | * new Query(
195 | * 'reactphp.org',
196 | * Message::TYPE_A,
197 | * Message::CLASS_IN
198 | * )
199 | * ];
200 | * ```
201 | *
202 | * @var Query[]
203 | */
204 | public $questions = [];
205 |
206 | /**
207 | * @var Record[]
208 | */
209 | public $answers = [];
210 |
211 | /**
212 | * @var Record[]
213 | */
214 | public $authority = [];
215 |
216 | /**
217 | * @var Record[]
218 | */
219 | public $additional = [];
220 | }
221 |
--------------------------------------------------------------------------------
/src/Model/Record.php:
--------------------------------------------------------------------------------
1 | name = $name;
148 | $this->type = $type;
149 | $this->class = $class;
150 | $this->ttl = $ttl;
151 | $this->data = $data;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/Protocol/BinaryDumper.php:
--------------------------------------------------------------------------------
1 | headerToBinary($message);
20 | $data .= $this->questionToBinary($message->questions);
21 | $data .= $this->recordsToBinary($message->answers);
22 | $data .= $this->recordsToBinary($message->authority);
23 | $data .= $this->recordsToBinary($message->additional);
24 |
25 | return $data;
26 | }
27 |
28 | /**
29 | * @param Message $message
30 | * @return string
31 | */
32 | private function headerToBinary(Message $message)
33 | {
34 | $data = '';
35 |
36 | $data .= pack('n', $message->id);
37 |
38 | $flags = 0x00;
39 | $flags = ($flags << 1) | ($message->qr ? 1 : 0);
40 | $flags = ($flags << 4) | $message->opcode;
41 | $flags = ($flags << 1) | ($message->aa ? 1 : 0);
42 | $flags = ($flags << 1) | ($message->tc ? 1 : 0);
43 | $flags = ($flags << 1) | ($message->rd ? 1 : 0);
44 | $flags = ($flags << 1) | ($message->ra ? 1 : 0);
45 | $flags = ($flags << 3) | 0; // skip unused zero bit
46 | $flags = ($flags << 4) | $message->rcode;
47 |
48 | $data .= pack('n', $flags);
49 |
50 | $data .= pack('n', count($message->questions));
51 | $data .= pack('n', count($message->answers));
52 | $data .= pack('n', count($message->authority));
53 | $data .= pack('n', count($message->additional));
54 |
55 | return $data;
56 | }
57 |
58 | /**
59 | * @param Query[] $questions
60 | * @return string
61 | */
62 | private function questionToBinary(array $questions)
63 | {
64 | $data = '';
65 |
66 | foreach ($questions as $question) {
67 | $data .= $this->domainNameToBinary($question->name);
68 | $data .= pack('n*', $question->type, $question->class);
69 | }
70 |
71 | return $data;
72 | }
73 |
74 | /**
75 | * @param Record[] $records
76 | * @return string
77 | */
78 | private function recordsToBinary(array $records)
79 | {
80 | $data = '';
81 |
82 | foreach ($records as $record) {
83 | /* @var $record Record */
84 | switch ($record->type) {
85 | case Message::TYPE_A:
86 | case Message::TYPE_AAAA:
87 | $binary = \inet_pton($record->data);
88 | break;
89 | case Message::TYPE_CNAME:
90 | case Message::TYPE_NS:
91 | case Message::TYPE_PTR:
92 | $binary = $this->domainNameToBinary($record->data);
93 | break;
94 | case Message::TYPE_TXT:
95 | case Message::TYPE_SPF:
96 | $binary = $this->textsToBinary($record->data);
97 | break;
98 | case Message::TYPE_MX:
99 | $binary = \pack(
100 | 'n',
101 | $record->data['priority']
102 | );
103 | $binary .= $this->domainNameToBinary($record->data['target']);
104 | break;
105 | case Message::TYPE_SRV:
106 | $binary = \pack(
107 | 'n*',
108 | $record->data['priority'],
109 | $record->data['weight'],
110 | $record->data['port']
111 | );
112 | $binary .= $this->domainNameToBinary($record->data['target']);
113 | break;
114 | case Message::TYPE_SOA:
115 | $binary = $this->domainNameToBinary($record->data['mname']);
116 | $binary .= $this->domainNameToBinary($record->data['rname']);
117 | $binary .= \pack(
118 | 'N*',
119 | $record->data['serial'],
120 | $record->data['refresh'],
121 | $record->data['retry'],
122 | $record->data['expire'],
123 | $record->data['minimum']
124 | );
125 | break;
126 | case Message::TYPE_CAA:
127 | $binary = \pack(
128 | 'C*',
129 | $record->data['flag'],
130 | \strlen($record->data['tag'])
131 | );
132 | $binary .= $record->data['tag'];
133 | $binary .= $record->data['value'];
134 | break;
135 | case Message::TYPE_SSHFP:
136 | $binary = \pack(
137 | 'CCH*',
138 | $record->data['algorithm'],
139 | $record->data['type'],
140 | $record->data['fingerprint']
141 | );
142 | break;
143 | case Message::TYPE_OPT:
144 | $binary = '';
145 | foreach ($record->data as $opt => $value) {
146 | if ($opt === Message::OPT_TCP_KEEPALIVE && $value !== null) {
147 | $value = \pack('n', round($value * 10));
148 | }
149 | $binary .= \pack('n*', $opt, \strlen((string) $value)) . $value;
150 | }
151 | break;
152 | default:
153 | // RDATA is already stored as binary value for unknown record types
154 | $binary = $record->data;
155 | }
156 |
157 | $data .= $this->domainNameToBinary($record->name);
158 | $data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary));
159 | $data .= $binary;
160 | }
161 |
162 | return $data;
163 | }
164 |
165 | /**
166 | * @param string[] $texts
167 | * @return string
168 | */
169 | private function textsToBinary(array $texts)
170 | {
171 | $data = '';
172 | foreach ($texts as $text) {
173 | $data .= \chr(\strlen($text)) . $text;
174 | }
175 | return $data;
176 | }
177 |
178 | /**
179 | * @param string $host
180 | * @return string
181 | */
182 | private function domainNameToBinary($host)
183 | {
184 | if ($host === '') {
185 | return "\0";
186 | }
187 |
188 | // break up domain name at each dot that is not preceeded by a backslash (escaped notation)
189 | return $this->textsToBinary(
190 | \array_map(
191 | 'stripcslashes',
192 | \preg_split(
193 | '/(?parse($data, 0);
27 | if ($message === null) {
28 | throw new InvalidArgumentException('Unable to parse binary message');
29 | }
30 |
31 | return $message;
32 | }
33 |
34 | /**
35 | * @param string $data
36 | * @param int $consumed
37 | * @return ?Message
38 | */
39 | private function parse($data, $consumed)
40 | {
41 | if (!isset($data[12 - 1])) {
42 | return null;
43 | }
44 |
45 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($data, 0, 12)));
46 |
47 | $message = new Message();
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 | $consumed += 12;
57 |
58 | // parse all questions
59 | for ($i = $qdCount; $i > 0; --$i) {
60 | list($question, $consumed) = $this->parseQuestion($data, $consumed);
61 | if ($question === null) {
62 | return null;
63 | } else {
64 | $message->questions[] = $question;
65 | }
66 | }
67 |
68 | // parse all answer records
69 | for ($i = $anCount; $i > 0; --$i) {
70 | list($record, $consumed) = $this->parseRecord($data, $consumed);
71 | if ($record === null) {
72 | return null;
73 | } else {
74 | $message->answers[] = $record;
75 | }
76 | }
77 |
78 | // parse all authority records
79 | for ($i = $nsCount; $i > 0; --$i) {
80 | list($record, $consumed) = $this->parseRecord($data, $consumed);
81 | if ($record === null) {
82 | return null;
83 | } else {
84 | $message->authority[] = $record;
85 | }
86 | }
87 |
88 | // parse all additional records
89 | for ($i = $arCount; $i > 0; --$i) {
90 | list($record, $consumed) = $this->parseRecord($data, $consumed);
91 | if ($record === null) {
92 | return null;
93 | } else {
94 | $message->additional[] = $record;
95 | }
96 | }
97 |
98 | return $message;
99 | }
100 |
101 | /**
102 | * @param string $data
103 | * @param int $consumed
104 | * @return array
105 | */
106 | private function parseQuestion($data, $consumed)
107 | {
108 | list($labels, $consumed) = $this->readLabels($data, $consumed);
109 |
110 | if ($labels === null || !isset($data[$consumed + 4 - 1])) {
111 | return [null, null];
112 | }
113 |
114 | list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
115 | $consumed += 4;
116 |
117 | return [
118 | new Query(
119 | implode('.', $labels),
120 | $type,
121 | $class
122 | ),
123 | $consumed
124 | ];
125 | }
126 |
127 | /**
128 | * @param string $data
129 | * @param int $consumed
130 | * @return array An array with a parsed Record on success or array with null if data is invalid/incomplete
131 | */
132 | private function parseRecord($data, $consumed)
133 | {
134 | list($name, $consumed) = $this->readDomain($data, $consumed);
135 |
136 | if ($name === null || !isset($data[$consumed + 10 - 1])) {
137 | return [null, null];
138 | }
139 |
140 | list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
141 | $consumed += 4;
142 |
143 | list($ttl) = array_values(unpack('N', substr($data, $consumed, 4)));
144 | $consumed += 4;
145 |
146 | // TTL is a UINT32 that must not have most significant bit set for BC reasons
147 | if ($ttl < 0 || $ttl >= 1 << 31) {
148 | $ttl = 0;
149 | }
150 |
151 | list($rdLength) = array_values(unpack('n', substr($data, $consumed, 2)));
152 | $consumed += 2;
153 |
154 | if (!isset($data[$consumed + $rdLength - 1])) {
155 | return [null, null];
156 | }
157 |
158 | $rdata = null;
159 | $expected = $consumed + $rdLength;
160 |
161 | if (Message::TYPE_A === $type) {
162 | if ($rdLength === 4) {
163 | $rdata = inet_ntop(substr($data, $consumed, $rdLength));
164 | $consumed += $rdLength;
165 | }
166 | } elseif (Message::TYPE_AAAA === $type) {
167 | if ($rdLength === 16) {
168 | $rdata = inet_ntop(substr($data, $consumed, $rdLength));
169 | $consumed += $rdLength;
170 | }
171 | } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
172 | list($rdata, $consumed) = $this->readDomain($data, $consumed);
173 | } elseif (Message::TYPE_TXT === $type || Message::TYPE_SPF === $type) {
174 | $rdata = [];
175 | while ($consumed < $expected) {
176 | $len = ord($data[$consumed]);
177 | $rdata[] = (string)substr($data, $consumed + 1, $len);
178 | $consumed += $len + 1;
179 | }
180 | } elseif (Message::TYPE_MX === $type) {
181 | if ($rdLength > 2) {
182 | list($priority) = array_values(unpack('n', substr($data, $consumed, 2)));
183 | list($target, $consumed) = $this->readDomain($data, $consumed + 2);
184 |
185 | $rdata = [
186 | 'priority' => $priority,
187 | 'target' => $target
188 | ];
189 | }
190 | } elseif (Message::TYPE_SRV === $type) {
191 | if ($rdLength > 6) {
192 | list($priority, $weight, $port) = array_values(unpack('n*', substr($data, $consumed, 6)));
193 | list($target, $consumed) = $this->readDomain($data, $consumed + 6);
194 |
195 | $rdata = [
196 | 'priority' => $priority,
197 | 'weight' => $weight,
198 | 'port' => $port,
199 | 'target' => $target
200 | ];
201 | }
202 | } elseif (Message::TYPE_SSHFP === $type) {
203 | if ($rdLength > 2) {
204 | list($algorithm, $hash) = \array_values(\unpack('C*', \substr($data, $consumed, 2)));
205 | $fingerprint = \bin2hex(\substr($data, $consumed + 2, $rdLength - 2));
206 | $consumed += $rdLength;
207 |
208 | $rdata = [
209 | 'algorithm' => $algorithm,
210 | 'type' => $hash,
211 | 'fingerprint' => $fingerprint
212 | ];
213 | }
214 | } elseif (Message::TYPE_SOA === $type) {
215 | list($mname, $consumed) = $this->readDomain($data, $consumed);
216 | list($rname, $consumed) = $this->readDomain($data, $consumed);
217 |
218 | if ($mname !== null && $rname !== null && isset($data[$consumed + 20 - 1])) {
219 | list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($data, $consumed, 20)));
220 | $consumed += 20;
221 |
222 | $rdata = [
223 | 'mname' => $mname,
224 | 'rname' => $rname,
225 | 'serial' => $serial,
226 | 'refresh' => $refresh,
227 | 'retry' => $retry,
228 | 'expire' => $expire,
229 | 'minimum' => $minimum
230 | ];
231 | }
232 | } elseif (Message::TYPE_OPT === $type) {
233 | $rdata = [];
234 | while (isset($data[$consumed + 4 - 1])) {
235 | list($code, $length) = array_values(unpack('n*', substr($data, $consumed, 4)));
236 | $value = (string) substr($data, $consumed + 4, $length);
237 | if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') {
238 | $value = null;
239 | } elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) {
240 | list($value) = array_values(unpack('n', $value));
241 | $value = round($value * 0.1, 1);
242 | } elseif ($code === Message::OPT_TCP_KEEPALIVE) {
243 | break;
244 | }
245 | $rdata[$code] = $value;
246 | $consumed += 4 + $length;
247 | }
248 | } elseif (Message::TYPE_CAA === $type) {
249 | if ($rdLength > 3) {
250 | list($flag, $tagLength) = array_values(unpack('C*', substr($data, $consumed, 2)));
251 |
252 | if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) {
253 | $tag = substr($data, $consumed + 2, $tagLength);
254 | $value = substr($data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
255 | $consumed += $rdLength;
256 |
257 | $rdata = [
258 | 'flag' => $flag,
259 | 'tag' => $tag,
260 | 'value' => $value
261 | ];
262 | }
263 | }
264 | } else {
265 | // unknown types simply parse rdata as an opaque binary string
266 | $rdata = substr($data, $consumed, $rdLength);
267 | $consumed += $rdLength;
268 | }
269 |
270 | // ensure parsing record data consumes expact number of bytes indicated in record length
271 | if ($consumed !== $expected || $rdata === null) {
272 | return [null, null];
273 | }
274 |
275 | return [
276 | new Record($name, $type, $class, $ttl, $rdata),
277 | $consumed
278 | ];
279 | }
280 |
281 | private function readDomain($data, $consumed)
282 | {
283 | list ($labels, $consumed) = $this->readLabels($data, $consumed);
284 |
285 | if ($labels === null) {
286 | return [null, null];
287 | }
288 |
289 | // use escaped notation for each label part, then join using dots
290 | return [
291 | \implode(
292 | '.',
293 | \array_map(
294 | function ($label) {
295 | return \addcslashes($label, "\0..\40.\177");
296 | },
297 | $labels
298 | )
299 | ),
300 | $consumed
301 | ];
302 | }
303 |
304 | /**
305 | * @param string $data
306 | * @param int $consumed
307 | * @param int $compressionDepth maximum depth for compressed labels to avoid unreasonable recursion
308 | * @return array
309 | */
310 | private function readLabels($data, $consumed, $compressionDepth = 127)
311 | {
312 | $labels = [];
313 |
314 | while (true) {
315 | if (!isset($data[$consumed])) {
316 | return [null, null];
317 | }
318 |
319 | $length = \ord($data[$consumed]);
320 |
321 | // end of labels reached
322 | if ($length === 0) {
323 | $consumed += 1;
324 | break;
325 | }
326 |
327 | // first two bits set? this is a compressed label (14 bit pointer offset)
328 | if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1]) && $compressionDepth) {
329 | $offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]);
330 | if ($offset >= $consumed) {
331 | return [null, null];
332 | }
333 |
334 | $consumed += 2;
335 | list($newLabels) = $this->readLabels($data, $offset, $compressionDepth - 1);
336 |
337 | if ($newLabels === null) {
338 | return [null, null];
339 | }
340 |
341 | $labels = array_merge($labels, $newLabels);
342 | break;
343 | }
344 |
345 | // length MUST be 0-63 (6 bits only) and data has to be large enough
346 | if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) {
347 | return [null, null];
348 | }
349 |
350 | $labels[] = substr($data, $consumed + 1, $length);
351 | $consumed += $length + 1;
352 | }
353 |
354 | return [$labels, $consumed];
355 | }
356 | }
357 |
--------------------------------------------------------------------------------
/src/Query/CachingExecutor.php:
--------------------------------------------------------------------------------
1 | executor = $executor;
24 | $this->cache = $cache;
25 | }
26 |
27 | public function query(Query $query)
28 | {
29 | $id = $query->name . ':' . $query->type . ':' . $query->class;
30 |
31 | $pending = $this->cache->get($id);
32 | return new Promise(function ($resolve, $reject) use ($query, $id, &$pending) {
33 | $pending->then(
34 | function ($message) use ($query, $id, &$pending) {
35 | // return cached response message on cache hit
36 | if ($message !== null) {
37 | return $message;
38 | }
39 |
40 | // perform DNS lookup if not already cached
41 | return $pending = $this->executor->query($query)->then(
42 | function (Message $message) use ($id) {
43 | // DNS response message received => store in cache when not truncated and return
44 | if (!$message->tc) {
45 | $this->cache->set($id, $message, $this->ttl($message));
46 | }
47 |
48 | return $message;
49 | }
50 | );
51 | }
52 | )->then($resolve, function ($e) use ($reject, &$pending) {
53 | $reject($e);
54 | $pending = null;
55 | });
56 | }, function ($_, $reject) use (&$pending, $query) {
57 | $reject(new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled'));
58 | $pending->cancel();
59 | $pending = null;
60 | });
61 | }
62 |
63 | /**
64 | * @param Message $message
65 | * @return int
66 | * @internal
67 | */
68 | public function ttl(Message $message)
69 | {
70 | // select TTL from answers (should all be the same), use smallest value if available
71 | // @link https://tools.ietf.org/html/rfc2181#section-5.2
72 | $ttl = null;
73 | foreach ($message->answers as $answer) {
74 | if ($ttl === null || $answer->ttl < $ttl) {
75 | $ttl = $answer->ttl;
76 | }
77 | }
78 |
79 | if ($ttl === null) {
80 | $ttl = self::TTL;
81 | }
82 |
83 | return $ttl;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/Query/CancellationException.php:
--------------------------------------------------------------------------------
1 | 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 | $promise->then(function () use ($key) {
62 | unset($this->pending[$key], $this->counts[$key]);
63 | }, function () use ($key) {
64 | unset($this->pending[$key], $this->counts[$key]);
65 | });
66 | }
67 |
68 | // Return a child promise awaiting the pending query.
69 | // Cancelling this child promise should only cancel the pending query
70 | // when no other child promise is awaiting the same query.
71 | return new Promise(function ($resolve, $reject) use ($promise) {
72 | $promise->then($resolve, $reject);
73 | }, function () use (&$promise, $key, $query) {
74 | if (--$this->counts[$key] < 1) {
75 | unset($this->pending[$key], $this->counts[$key]);
76 | $promise->cancel();
77 | $promise = null;
78 | }
79 | throw new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled');
80 | });
81 | }
82 |
83 | private function serializeQueryToIdentity(Query $query)
84 | {
85 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Query/ExecutorInterface.php:
--------------------------------------------------------------------------------
1 | query($query)->then(
17 | * function (React\Dns\Model\Message $response) {
18 | * // response message successfully received
19 | * var_dump($response->rcode, $response->answers);
20 | * },
21 | * function (Exception $error) {
22 | * // failed to query due to $error
23 | * }
24 | * );
25 | * ```
26 | *
27 | * The returned Promise MUST be implemented in such a way that it can be
28 | * cancelled when it is still pending. Cancelling a pending promise MUST
29 | * reject its value with an Exception. It SHOULD clean up any underlying
30 | * resources and references as applicable.
31 | *
32 | * ```php
33 | * $promise = $executor->query($query);
34 | *
35 | * $promise->cancel();
36 | * ```
37 | *
38 | * @param Query $query
39 | * @return \React\Promise\PromiseInterface<\React\Dns\Model\Message>
40 | * resolves with response message on success or rejects with an Exception on error
41 | */
42 | public function query(Query $query);
43 | }
44 |
--------------------------------------------------------------------------------
/src/Query/FallbackExecutor.php:
--------------------------------------------------------------------------------
1 | executor = $executor;
15 | $this->fallback = $fallback;
16 | }
17 |
18 | public function query(Query $query)
19 | {
20 | $cancelled = false;
21 | $promise = $this->executor->query($query);
22 |
23 | return new Promise(function ($resolve, $reject) use (&$promise, $query, &$cancelled) {
24 | $promise->then($resolve, function (\Exception $e1) use ($query, $resolve, $reject, &$cancelled, &$promise) {
25 | // reject if primary resolution rejected due to cancellation
26 | if ($cancelled) {
27 | $reject($e1);
28 | return;
29 | }
30 |
31 | // start fallback query if primary query rejected
32 | $promise = $this->fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) {
33 | $append = $e2->getMessage();
34 | if (($pos = strpos($append, ':')) !== false) {
35 | $append = substr($append, $pos + 2);
36 | }
37 |
38 | // reject with combined error message if both queries fail
39 | $reject(new \RuntimeException($e1->getMessage() . '. ' . $append));
40 | });
41 | });
42 | }, function () use (&$promise, &$cancelled) {
43 | // cancel pending query (primary or fallback)
44 | $cancelled = true;
45 | $promise->cancel();
46 | });
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Query/HostsFileExecutor.php:
--------------------------------------------------------------------------------
1 | hosts = $hosts;
25 | $this->fallback = $fallback;
26 | }
27 |
28 | public function query(Query $query)
29 | {
30 | if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
31 | // forward lookup for type A or AAAA
32 | $records = [];
33 | $expectsColon = $query->type === Message::TYPE_AAAA;
34 | foreach ($this->hosts->getIpsForHost($query->name) as $ip) {
35 | // ensure this is an IPv4/IPV6 address according to query type
36 | if ((strpos($ip, ':') !== false) === $expectsColon) {
37 | $records[] = new Record($query->name, $query->type, $query->class, 0, $ip);
38 | }
39 | }
40 |
41 | if ($records) {
42 | return resolve(
43 | Message::createResponseWithAnswersForQuery($query, $records)
44 | );
45 | }
46 | } elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) {
47 | // reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain
48 | $ip = $this->getIpFromHost($query->name);
49 |
50 | if ($ip !== null) {
51 | $records = [];
52 | foreach ($this->hosts->getHostsForIp($ip) as $host) {
53 | $records[] = new Record($query->name, $query->type, $query->class, 0, $host);
54 | }
55 |
56 | if ($records) {
57 | return resolve(
58 | Message::createResponseWithAnswersForQuery($query, $records)
59 | );
60 | }
61 | }
62 | }
63 |
64 | return $this->fallback->query($query);
65 | }
66 |
67 | private function getIpFromHost($host)
68 | {
69 | if (substr($host, -13) === '.in-addr.arpa') {
70 | // IPv4: read as IP and reverse bytes
71 | $ip = @inet_pton(substr($host, 0, -13));
72 | if ($ip === false || isset($ip[4])) {
73 | return null;
74 | }
75 |
76 | return inet_ntop(strrev($ip));
77 | } elseif (substr($host, -9) === '.ip6.arpa') {
78 | // IPv6: replace dots, reverse nibbles and interpret as hexadecimal string
79 | $ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9)))));
80 | if ($ip === false) {
81 | return null;
82 | }
83 |
84 | return $ip;
85 | } else {
86 | return null;
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Query/Query.php:
--------------------------------------------------------------------------------
1 | name = $name;
41 | $this->type = $type;
42 | $this->class = $class;
43 | }
44 |
45 | /**
46 | * Describes the hostname and query type/class for this query
47 | *
48 | * The output format is supposed to be human readable and is subject to change.
49 | * The format is inspired by RFC 3597 when handling unkown types/classes.
50 | *
51 | * @return string "example.com (A)" or "example.com (CLASS0 TYPE1234)"
52 | * @link https://tools.ietf.org/html/rfc3597
53 | */
54 | public function describe()
55 | {
56 | $class = $this->class !== Message::CLASS_IN ? 'CLASS' . $this->class . ' ' : '';
57 |
58 | $type = 'TYPE' . $this->type;
59 | $ref = new \ReflectionClass(Message::class);
60 | foreach ($ref->getConstants() as $name => $value) {
61 | if ($value === $this->type && \strpos($name, 'TYPE_') === 0) {
62 | $type = \substr($name, 5);
63 | break;
64 | }
65 | }
66 |
67 | return $this->name . ' (' . $class . $type . ')';
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/Query/RetryExecutor.php:
--------------------------------------------------------------------------------
1 | executor = $executor;
16 | $this->retries = $retries;
17 | }
18 |
19 | public function query(Query $query)
20 | {
21 | return $this->tryQuery($query, $this->retries);
22 | }
23 |
24 | public function tryQuery(Query $query, $retries)
25 | {
26 | $deferred = new Deferred(function () use (&$promise) {
27 | if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
28 | $promise->cancel();
29 | }
30 | });
31 |
32 | $success = function ($value) use ($deferred, &$errorback) {
33 | $errorback = null;
34 | $deferred->resolve($value);
35 | };
36 |
37 | $errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries) {
38 | if (!$e instanceof TimeoutException) {
39 | $errorback = null;
40 | $deferred->reject($e);
41 | } elseif ($retries <= 0) {
42 | $errorback = null;
43 | $deferred->reject($e = new \RuntimeException(
44 | 'DNS query for ' . $query->describe() . ' failed: too many retries',
45 | 0,
46 | $e
47 | ));
48 |
49 | // avoid garbage references by replacing all closures in call stack.
50 | // what a lovely piece of code!
51 | $r = new \ReflectionProperty(\Exception::class, 'trace');
52 | $r->setAccessible(true);
53 | $trace = $r->getValue($e);
54 |
55 | // Exception trace arguments are not available on some PHP 7.4 installs
56 | // @codeCoverageIgnoreStart
57 | foreach ($trace as $ti => $one) {
58 | if (isset($one['args'])) {
59 | foreach ($one['args'] as $ai => $arg) {
60 | if ($arg instanceof \Closure) {
61 | $trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
62 | }
63 | }
64 | }
65 | }
66 | // @codeCoverageIgnoreEnd
67 | $r->setValue($e, $trace);
68 | } else {
69 | --$retries;
70 | $promise = $this->executor->query($query)->then(
71 | $success,
72 | $errorback
73 | );
74 | }
75 | };
76 |
77 | $promise = $this->executor->query($query)->then(
78 | $success,
79 | $errorback
80 | );
81 |
82 | return $deferred->promise();
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/Query/SelectiveTransportExecutor.php:
--------------------------------------------------------------------------------
1 | query(
22 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
23 | * )->then(function (Message $message) {
24 | * foreach ($message->answers as $answer) {
25 | * echo 'IPv6: ' . $answer->data . PHP_EOL;
26 | * }
27 | * }, 'printf');
28 | * ```
29 | *
30 | * Note that this executor only implements the logic to select the correct
31 | * transport for the given DNS query. Implementing the correct transport logic,
32 | * implementing timeouts and any retry logic is left up to the given executors,
33 | * see also [`UdpTransportExecutor`](#udptransportexecutor) and
34 | * [`TcpTransportExecutor`](#tcptransportexecutor) for more details.
35 | *
36 | * Note that this executor is entirely async and as such allows you to execute
37 | * any number of queries concurrently. You should probably limit the number of
38 | * concurrent queries in your application or you're very likely going to face
39 | * rate limitations and bans on the resolver end. For many common applications,
40 | * you may want to avoid sending the same query multiple times when the first
41 | * one is still pending, so you will likely want to use this in combination with
42 | * a `CoopExecutor` like this:
43 | *
44 | * ```php
45 | * $executor = new CoopExecutor(
46 | * new SelectiveTransportExecutor(
47 | * $datagramExecutor,
48 | * $streamExecutor
49 | * )
50 | * );
51 | * ```
52 | */
53 | class SelectiveTransportExecutor implements ExecutorInterface
54 | {
55 | private $datagramExecutor;
56 | private $streamExecutor;
57 |
58 | public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor)
59 | {
60 | $this->datagramExecutor = $datagramExecutor;
61 | $this->streamExecutor = $streamExecutor;
62 | }
63 |
64 | public function query(Query $query)
65 | {
66 | $pending = $this->datagramExecutor->query($query);
67 |
68 | return new Promise(function ($resolve, $reject) use (&$pending, $query) {
69 | $pending->then(
70 | $resolve,
71 | function ($e) use (&$pending, $query, $resolve, $reject) {
72 | if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) {
73 | $pending = $this->streamExecutor->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 | }
85 |
--------------------------------------------------------------------------------
/src/Query/TcpTransportExecutor.php:
--------------------------------------------------------------------------------
1 | query(
25 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
26 | * )->then(function (Message $message) {
27 | * foreach ($message->answers as $answer) {
28 | * echo 'IPv6: ' . $answer->data . PHP_EOL;
29 | * }
30 | * }, 'printf');
31 | * ```
32 | *
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),
41 | * 3.0
42 | * );
43 | * ```
44 | *
45 | * Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
46 | * transport, so you do not necessarily have to implement any retry logic.
47 | *
48 | * Note that this executor is entirely async and as such allows you to execute
49 | * queries concurrently. The first query will establish a TCP/IP socket
50 | * connection to the DNS server which will be kept open for a short period.
51 | * Additional queries will automatically reuse this existing socket connection
52 | * to the DNS server, will pipeline multiple requests over this single
53 | * connection and will keep an idle connection open for a short period. The
54 | * initial TCP/IP connection overhead may incur a slight delay if you only send
55 | * occasional queries – when sending a larger number of concurrent queries over
56 | * an existing connection, it becomes increasingly more efficient and avoids
57 | * creating many concurrent sockets like the UDP-based executor. You may still
58 | * want to limit the number of (concurrent) queries in your application or you
59 | * may be facing rate limitations and bans on the resolver end. For many common
60 | * applications, you may want to avoid sending the same query multiple times
61 | * when the first one is still pending, so you will likely want to use this in
62 | * combination with a `CoopExecutor` like this:
63 | *
64 | * ```php
65 | * $executor = new CoopExecutor(
66 | * new TimeoutExecutor(
67 | * new TcpTransportExecutor($nameserver),
68 | * 3.0
69 | * )
70 | * );
71 | * ```
72 | *
73 | * > Internally, this class uses PHP's TCP/IP sockets and does not take advantage
74 | * of [react/socket](https://github.com/reactphp/socket) purely for
75 | * organizational reasons to avoid a cyclic dependency between the two
76 | * packages. Higher-level components should take advantage of the Socket
77 | * component instead of reimplementing this socket logic from scratch.
78 | */
79 | class TcpTransportExecutor implements ExecutorInterface
80 | {
81 | private $nameserver;
82 | private $loop;
83 | private $parser;
84 | private $dumper;
85 |
86 | /**
87 | * @var ?resource
88 | */
89 | private $socket;
90 |
91 | /**
92 | * @var Deferred[]
93 | */
94 | private $pending = [];
95 |
96 | /**
97 | * @var string[]
98 | */
99 | private $names = [];
100 |
101 | /**
102 | * Maximum idle time when socket is current unused (i.e. no pending queries outstanding)
103 | *
104 | * If a new query is to be sent during the idle period, we can reuse the
105 | * existing socket without having to wait for a new socket connection.
106 | * This uses a rather small, hard-coded value to not keep any unneeded
107 | * sockets open and to not keep the loop busy longer than needed.
108 | *
109 | * A future implementation may take advantage of `edns-tcp-keepalive` to keep
110 | * the socket open for longer periods. This will likely require explicit
111 | * configuration because this may consume additional resources and also keep
112 | * the loop busy for longer than expected in some applications.
113 | *
114 | * @var float
115 | * @link https://tools.ietf.org/html/rfc7766#section-6.2.1
116 | * @link https://tools.ietf.org/html/rfc7828
117 | */
118 | private $idlePeriod = 0.001;
119 |
120 | /**
121 | * @var ?\React\EventLoop\TimerInterface
122 | */
123 | private $idleTimer;
124 |
125 | private $writeBuffer = '';
126 | private $writePending = false;
127 |
128 | private $readBuffer = '';
129 | private $readPending = false;
130 |
131 | /** @var string */
132 | private $readChunk = 0xffff;
133 |
134 | /**
135 | * @param string $nameserver
136 | * @param ?LoopInterface $loop
137 | */
138 | public function __construct($nameserver, ?LoopInterface $loop = null)
139 | {
140 | if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
141 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
142 | $nameserver = '[' . $nameserver . ']';
143 | }
144 |
145 | $parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver);
146 | if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
147 | throw new \InvalidArgumentException('Invalid nameserver address given');
148 | }
149 |
150 | $this->nameserver = 'tcp://' . $parts['host'] . ':' . ($parts['port'] ?? 53);
151 | $this->loop = $loop ?: Loop::get();
152 | $this->parser = new Parser();
153 | $this->dumper = new BinaryDumper();
154 | }
155 |
156 | public function query(Query $query)
157 | {
158 | $request = Message::createRequestForQuery($query);
159 |
160 | // keep shuffing message ID to avoid using the same message ID for two pending queries at the same time
161 | while (isset($this->pending[$request->id])) {
162 | $request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore
163 | }
164 |
165 | $queryData = $this->dumper->toBinary($request);
166 | $length = \strlen($queryData);
167 | if ($length > 0xffff) {
168 | return reject(new \RuntimeException(
169 | 'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport'
170 | ));
171 | }
172 |
173 | $queryData = \pack('n', $length) . $queryData;
174 |
175 | if ($this->socket === null) {
176 | // create async TCP/IP connection (may take a while)
177 | $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT);
178 | if ($socket === false) {
179 | return reject(new \RuntimeException(
180 | 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
181 | $errno
182 | ));
183 | }
184 |
185 | // set socket to non-blocking and wait for it to become writable (connection success/rejected)
186 | \stream_set_blocking($socket, false);
187 | if (\function_exists('stream_set_chunk_size')) {
188 | \stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore
189 | }
190 | $this->socket = $socket;
191 | }
192 |
193 | if ($this->idleTimer !== null) {
194 | $this->loop->cancelTimer($this->idleTimer);
195 | $this->idleTimer = null;
196 | }
197 |
198 | // wait for socket to become writable to actually write out data
199 | $this->writeBuffer .= $queryData;
200 | if (!$this->writePending) {
201 | $this->writePending = true;
202 | $this->loop->addWriteStream($this->socket, [$this, 'handleWritable']);
203 | }
204 |
205 | $deferred = new Deferred(function () use ($request) {
206 | // remove from list of pending names, but remember pending query
207 | $name = $this->names[$request->id];
208 | unset($this->names[$request->id]);
209 | $this->checkIdle();
210 |
211 | throw new CancellationException('DNS query for ' . $name . ' has been cancelled');
212 | });
213 |
214 | $this->pending[$request->id] = $deferred;
215 | $this->names[$request->id] = $query->describe();
216 |
217 | return $deferred->promise();
218 | }
219 |
220 | /**
221 | * @internal
222 | */
223 | public function handleWritable()
224 | {
225 | if ($this->readPending === false) {
226 | $name = @\stream_socket_get_name($this->socket, true);
227 | if ($name === false) {
228 | // Connection failed? Check socket error if available for underlying errno/errstr.
229 | // @codeCoverageIgnoreStart
230 | if (\function_exists('socket_import_stream')) {
231 | $socket = \socket_import_stream($this->socket);
232 | $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
233 | $errstr = \socket_strerror($errno);
234 | } else {
235 | $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
236 | $errstr = 'Connection refused';
237 | }
238 | // @codeCoverageIgnoreEnd
239 |
240 | $this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno);
241 | return;
242 | }
243 |
244 | $this->readPending = true;
245 | $this->loop->addReadStream($this->socket, [$this, 'handleRead']);
246 | }
247 |
248 | $errno = 0;
249 | $errstr = '';
250 | \set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
251 | // Match errstr from PHP's warning message.
252 | // fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe
253 | \preg_match('/errno=(\d+) (.+)/', $error, $m);
254 | $errno = (int) ($m[1] ?? 0);
255 | $errstr = $m[2] ?? $error;
256 | });
257 |
258 | $written = \fwrite($this->socket, $this->writeBuffer);
259 |
260 | \restore_error_handler();
261 |
262 | if ($written === false || $written === 0) {
263 | $this->closeError(
264 | 'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
265 | $errno
266 | );
267 | return;
268 | }
269 |
270 | if (isset($this->writeBuffer[$written])) {
271 | $this->writeBuffer = \substr($this->writeBuffer, $written);
272 | } else {
273 | $this->loop->removeWriteStream($this->socket);
274 | $this->writePending = false;
275 | $this->writeBuffer = '';
276 | }
277 | }
278 |
279 | /**
280 | * @internal
281 | */
282 | public function handleRead()
283 | {
284 | // read one chunk of data from the DNS server
285 | // any error is fatal, this is a stream of TCP/IP data
286 | $chunk = @\fread($this->socket, $this->readChunk);
287 | if ($chunk === false || $chunk === '') {
288 | $this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
289 | return;
290 | }
291 |
292 | // reassemble complete message by concatenating all chunks.
293 | $this->readBuffer .= $chunk;
294 |
295 | // response message header contains at least 12 bytes
296 | while (isset($this->readBuffer[11])) {
297 | // read response message length from first 2 bytes and ensure we have length + data in buffer
298 | list(, $length) = \unpack('n', $this->readBuffer);
299 | if (!isset($this->readBuffer[$length + 1])) {
300 | return;
301 | }
302 |
303 | $data = \substr($this->readBuffer, 2, $length);
304 | $this->readBuffer = (string)substr($this->readBuffer, $length + 2);
305 |
306 | try {
307 | $response = $this->parser->parseMessage($data);
308 | } catch (\Exception $e) {
309 | // reject all pending queries if we received an invalid message from remote server
310 | $this->closeError('Invalid message received from DNS server ' . $this->nameserver);
311 | return;
312 | }
313 |
314 | // reject all pending queries if we received an unexpected response ID or truncated response
315 | if (!isset($this->pending[$response->id]) || $response->tc) {
316 | $this->closeError('Invalid response message received from DNS server ' . $this->nameserver);
317 | return;
318 | }
319 |
320 | $deferred = $this->pending[$response->id];
321 | unset($this->pending[$response->id], $this->names[$response->id]);
322 |
323 | $deferred->resolve($response);
324 |
325 | $this->checkIdle();
326 | }
327 | }
328 |
329 | /**
330 | * @internal
331 | * @param string $reason
332 | * @param int $code
333 | */
334 | public function closeError($reason, $code = 0)
335 | {
336 | $this->readBuffer = '';
337 | if ($this->readPending) {
338 | $this->loop->removeReadStream($this->socket);
339 | $this->readPending = false;
340 | }
341 |
342 | $this->writeBuffer = '';
343 | if ($this->writePending) {
344 | $this->loop->removeWriteStream($this->socket);
345 | $this->writePending = false;
346 | }
347 |
348 | if ($this->idleTimer !== null) {
349 | $this->loop->cancelTimer($this->idleTimer);
350 | $this->idleTimer = null;
351 | }
352 |
353 | @\fclose($this->socket);
354 | $this->socket = null;
355 |
356 | foreach ($this->names as $id => $name) {
357 | $this->pending[$id]->reject(new \RuntimeException(
358 | 'DNS query for ' . $name . ' failed: ' . $reason,
359 | $code
360 | ));
361 | }
362 | $this->pending = $this->names = [];
363 | }
364 |
365 | /**
366 | * @internal
367 | */
368 | public function checkIdle()
369 | {
370 | if ($this->idleTimer === null && !$this->names) {
371 | $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () {
372 | $this->closeError('Idle timeout');
373 | });
374 | }
375 | }
376 | }
377 |
--------------------------------------------------------------------------------
/src/Query/TimeoutException.php:
--------------------------------------------------------------------------------
1 | executor = $executor;
18 | $this->loop = $loop ?: Loop::get();
19 | $this->timeout = $timeout;
20 | }
21 |
22 | public function query(Query $query)
23 | {
24 | $promise = $this->executor->query($query);
25 |
26 | return new Promise(function ($resolve, $reject) use ($promise, $query) {
27 | $timer = null;
28 | $promise = $promise->then(function ($v) use (&$timer, $resolve) {
29 | if ($timer) {
30 | $this->loop->cancelTimer($timer);
31 | }
32 | $timer = false;
33 | $resolve($v);
34 | }, function ($v) use (&$timer, $reject) {
35 | if ($timer) {
36 | $this->loop->cancelTimer($timer);
37 | }
38 | $timer = false;
39 | $reject($v);
40 | });
41 |
42 | // promise already resolved => no need to start timer
43 | if ($timer === false) {
44 | return;
45 | }
46 |
47 | // start timeout timer which will cancel the pending promise
48 | $timer = $this->loop->addTimer($this->timeout, function () use (&$promise, $reject, $query) {
49 | $reject(new TimeoutException(
50 | 'DNS query for ' . $query->describe() . ' timed out'
51 | ));
52 |
53 | // Cancel pending query to clean up any underlying resources and references.
54 | // Avoid garbage references in call stack by passing pending promise by reference.
55 | assert(\method_exists($promise, 'cancel'));
56 | $promise->cancel();
57 | $promise = null;
58 | });
59 | }, function () use (&$promise) {
60 | // Cancelling this promise will cancel the pending query, thus triggering the rejection logic above.
61 | // Avoid garbage references in call stack by passing pending promise by reference.
62 | assert(\method_exists($promise, 'cancel'));
63 | $promise->cancel();
64 | $promise = null;
65 | });
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/Query/UdpTransportExecutor.php:
--------------------------------------------------------------------------------
1 | query(
26 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
27 | * )->then(function (Message $message) {
28 | * foreach ($message->answers as $answer) {
29 | * echo 'IPv6: ' . $answer->data . PHP_EOL;
30 | * }
31 | * }, 'printf');
32 | * ```
33 | *
34 | * See also the [fourth example](examples).
35 | *
36 | * Note that this executor does not implement a timeout, so you will very likely
37 | * want to use this in combination with a `TimeoutExecutor` like this:
38 | *
39 | * ```php
40 | * $executor = new TimeoutExecutor(
41 | * new UdpTransportExecutor($nameserver),
42 | * 3.0
43 | * );
44 | * ```
45 | *
46 | * Also note that this executor uses an unreliable UDP transport and that it
47 | * does not implement any retry logic, so you will likely want to use this in
48 | * combination with a `RetryExecutor` like this:
49 | *
50 | * ```php
51 | * $executor = new RetryExecutor(
52 | * new TimeoutExecutor(
53 | * new UdpTransportExecutor($nameserver),
54 | * 3.0
55 | * )
56 | * );
57 | * ```
58 | *
59 | * Note that this executor is entirely async and as such allows you to execute
60 | * any number of queries concurrently. You should probably limit the number of
61 | * concurrent queries in your application or you're very likely going to face
62 | * rate limitations and bans on the resolver end. For many common applications,
63 | * you may want to avoid sending the same query multiple times when the first
64 | * one is still pending, so you will likely want to use this in combination with
65 | * a `CoopExecutor` like this:
66 | *
67 | * ```php
68 | * $executor = new CoopExecutor(
69 | * new RetryExecutor(
70 | * new TimeoutExecutor(
71 | * new UdpTransportExecutor($nameserver),
72 | * 3.0
73 | * )
74 | * )
75 | * );
76 | * ```
77 | *
78 | * > Internally, this class uses PHP's UDP sockets and does not take advantage
79 | * of [react/datagram](https://github.com/reactphp/datagram) purely for
80 | * organizational reasons to avoid a cyclic dependency between the two
81 | * packages. Higher-level components should take advantage of the Datagram
82 | * component instead of reimplementing this socket logic from scratch.
83 | */
84 | final class UdpTransportExecutor implements ExecutorInterface
85 | {
86 | private $nameserver;
87 | private $loop;
88 | private $parser;
89 | private $dumper;
90 |
91 | /**
92 | * maximum UDP packet size to send and receive
93 | *
94 | * @var int
95 | */
96 | private $maxPacketSize = 512;
97 |
98 | /**
99 | * @param string $nameserver
100 | * @param ?LoopInterface $loop
101 | */
102 | public function __construct($nameserver, ?LoopInterface $loop = null)
103 | {
104 | if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
105 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
106 | $nameserver = '[' . $nameserver . ']';
107 | }
108 |
109 | $parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver);
110 | if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
111 | throw new \InvalidArgumentException('Invalid nameserver address given');
112 | }
113 |
114 | $this->nameserver = 'udp://' . $parts['host'] . ':' . ($parts['port'] ?? 53);
115 | $this->loop = $loop ?: Loop::get();
116 | $this->parser = new Parser();
117 | $this->dumper = new BinaryDumper();
118 | }
119 |
120 | public function query(Query $query)
121 | {
122 | $request = Message::createRequestForQuery($query);
123 |
124 | $queryData = $this->dumper->toBinary($request);
125 | if (isset($queryData[$this->maxPacketSize])) {
126 | return reject(new \RuntimeException(
127 | 'DNS query for ' . $query->describe() . ' failed: Query too large for UDP transport',
128 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
129 | ));
130 | }
131 |
132 | // UDP connections are instant, so try connection without a loop or timeout
133 | $errno = 0;
134 | $errstr = '';
135 | $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0);
136 | if ($socket === false) {
137 | return reject(new \RuntimeException(
138 | 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
139 | $errno
140 | ));
141 | }
142 |
143 | // set socket to non-blocking and immediately try to send (fill write buffer)
144 | \stream_set_blocking($socket, false);
145 |
146 | \set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
147 | // Write may potentially fail, but most common errors are already caught by connection check above.
148 | // Among others, macOS is known to report here when trying to send to broadcast address.
149 | // This can also be reproduced by writing data exceeding `stream_set_chunk_size()` to a server refusing UDP data.
150 | // fwrite(): send of 8192 bytes failed with errno=111 Connection refused
151 | \preg_match('/errno=(\d+) (.+)/', $error, $m);
152 | $errno = (int) ($m[1] ?? 0);
153 | $errstr = $m[2] ?? $error;
154 | });
155 |
156 | $written = \fwrite($socket, $queryData);
157 |
158 | \restore_error_handler();
159 |
160 | if ($written !== \strlen($queryData)) {
161 | return reject(new \RuntimeException(
162 | 'DNS query for ' . $query->describe() . ' failed: Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
163 | $errno
164 | ));
165 | }
166 |
167 | $deferred = new Deferred(function () use ($socket, $query) {
168 | // cancellation should remove socket from loop and close socket
169 | $this->loop->removeReadStream($socket);
170 | \fclose($socket);
171 |
172 | throw new CancellationException('DNS query for ' . $query->describe() . ' has been cancelled');
173 | });
174 |
175 | $this->loop->addReadStream($socket, function ($socket) use ($deferred, $query, $request) {
176 | // try to read a single data packet from the DNS server
177 | // ignoring any errors, this is uses UDP packets and not a stream of data
178 | $data = @\fread($socket, $this->maxPacketSize);
179 | if ($data === false) {
180 | return;
181 | }
182 |
183 | try {
184 | $response = $this->parser->parseMessage($data);
185 | } catch (\Exception $e) {
186 | // ignore and await next if we received an invalid message from remote server
187 | // this may as well be a fake response from an attacker (possible DOS)
188 | return;
189 | }
190 |
191 | // ignore and await next if we received an unexpected response ID
192 | // this may as well be a fake response from an attacker (possible cache poisoning)
193 | if ($response->id !== $request->id) {
194 | return;
195 | }
196 |
197 | // we only react to the first valid message, so remove socket from loop and close
198 | $this->loop->removeReadStream($socket);
199 | \fclose($socket);
200 |
201 | if ($response->tc) {
202 | $deferred->reject(new \RuntimeException(
203 | 'DNS query for ' . $query->describe() . ' failed: The DNS server ' . $this->nameserver . ' returned a truncated result for a UDP query',
204 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
205 | ));
206 | return;
207 | }
208 |
209 | $deferred->resolve($response);
210 | });
211 |
212 | return $deferred->promise();
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/RecordNotFoundException.php:
--------------------------------------------------------------------------------
1 | decorateHostsFileExecutor($this->createExecutor($config, $loop ?: Loop::get()));
42 |
43 | return new Resolver($executor);
44 | }
45 |
46 | /**
47 | * Creates a cached DNS resolver instance for the given DNS config and cache
48 | *
49 | * As of v1.7.0 it's recommended to pass a `Config` object instead of a
50 | * single nameserver address. If the given config contains more than one DNS
51 | * nameserver, all DNS nameservers will be used in order. The primary DNS
52 | * server will always be used first before falling back to the secondary or
53 | * tertiary DNS server.
54 | *
55 | * @param Config|string $config DNS Config object (recommended) or single nameserver address
56 | * @param ?LoopInterface $loop
57 | * @param ?CacheInterface $cache
58 | * @return \React\Dns\Resolver\ResolverInterface
59 | * @throws \InvalidArgumentException for invalid DNS server address
60 | * @throws \UnderflowException when given DNS Config object has an empty list of nameservers
61 | */
62 | public function createCached($config, ?LoopInterface $loop = null, ?CacheInterface $cache = null)
63 | {
64 | // default to keeping maximum of 256 responses in cache unless explicitly given
65 | if (!($cache instanceof CacheInterface)) {
66 | $cache = new ArrayCache(256);
67 | }
68 |
69 | $executor = $this->createExecutor($config, $loop ?: Loop::get());
70 | $executor = new CachingExecutor($executor, $cache);
71 | $executor = $this->decorateHostsFileExecutor($executor);
72 |
73 | return new Resolver($executor);
74 | }
75 |
76 | /**
77 | * Tries to load the hosts file and decorates the given executor on success
78 | *
79 | * @param ExecutorInterface $executor
80 | * @return ExecutorInterface
81 | * @codeCoverageIgnore
82 | */
83 | private function decorateHostsFileExecutor(ExecutorInterface $executor)
84 | {
85 | try {
86 | $executor = new HostsFileExecutor(
87 | HostsFile::loadFromPathBlocking(),
88 | $executor
89 | );
90 | } catch (\RuntimeException $e) {
91 | // ignore this file if it can not be loaded
92 | }
93 |
94 | // Windows does not store localhost in hosts file by default but handles this internally
95 | // To compensate for this, we explicitly use hard-coded defaults for localhost
96 | if (DIRECTORY_SEPARATOR === '\\') {
97 | $executor = new HostsFileExecutor(
98 | new HostsFile("127.0.0.1 localhost\n::1 localhost"),
99 | $executor
100 | );
101 | }
102 |
103 | return $executor;
104 | }
105 |
106 | /**
107 | * @param Config|string $nameserver
108 | * @param LoopInterface $loop
109 | * @return CoopExecutor
110 | * @throws \InvalidArgumentException for invalid DNS server address
111 | * @throws \UnderflowException when given DNS Config object has an empty list of nameservers
112 | */
113 | private function createExecutor($nameserver, LoopInterface $loop)
114 | {
115 | if ($nameserver instanceof Config) {
116 | if (!$nameserver->nameservers) {
117 | throw new \UnderflowException('Empty config with no DNS servers');
118 | }
119 |
120 | // Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config).
121 | // Note to future self: Recursion isn't too hard, but how deep do we really want to go?
122 | $primary = reset($nameserver->nameservers);
123 | $secondary = next($nameserver->nameservers);
124 | $tertiary = next($nameserver->nameservers);
125 |
126 | if ($tertiary !== false) {
127 | // 3 DNS servers given => nest first with fallback for second and third
128 | return new CoopExecutor(
129 | new RetryExecutor(
130 | new FallbackExecutor(
131 | $this->createSingleExecutor($primary, $loop),
132 | new FallbackExecutor(
133 | $this->createSingleExecutor($secondary, $loop),
134 | $this->createSingleExecutor($tertiary, $loop)
135 | )
136 | )
137 | )
138 | );
139 | } elseif ($secondary !== false) {
140 | // 2 DNS servers given => fallback from first to second
141 | return new CoopExecutor(
142 | new RetryExecutor(
143 | new FallbackExecutor(
144 | $this->createSingleExecutor($primary, $loop),
145 | $this->createSingleExecutor($secondary, $loop)
146 | )
147 | )
148 | );
149 | } else {
150 | // 1 DNS server given => use single executor
151 | $nameserver = $primary;
152 | }
153 | }
154 |
155 | return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop)));
156 | }
157 |
158 | /**
159 | * @param string $nameserver
160 | * @param LoopInterface $loop
161 | * @return ExecutorInterface
162 | * @throws \InvalidArgumentException for invalid DNS server address
163 | */
164 | private function createSingleExecutor($nameserver, LoopInterface $loop)
165 | {
166 | $parts = \parse_url($nameserver);
167 |
168 | if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') {
169 | $executor = $this->createTcpExecutor($nameserver, $loop);
170 | } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') {
171 | $executor = $this->createUdpExecutor($nameserver, $loop);
172 | } else {
173 | $executor = new SelectiveTransportExecutor(
174 | $this->createUdpExecutor($nameserver, $loop),
175 | $this->createTcpExecutor($nameserver, $loop)
176 | );
177 | }
178 |
179 | return $executor;
180 | }
181 |
182 | /**
183 | * @param string $nameserver
184 | * @param LoopInterface $loop
185 | * @return TimeoutExecutor
186 | * @throws \InvalidArgumentException for invalid DNS server address
187 | */
188 | private function createTcpExecutor($nameserver, LoopInterface $loop)
189 | {
190 | return new TimeoutExecutor(
191 | new TcpTransportExecutor($nameserver, $loop),
192 | 5.0,
193 | $loop
194 | );
195 | }
196 |
197 | /**
198 | * @param string $nameserver
199 | * @param LoopInterface $loop
200 | * @return TimeoutExecutor
201 | * @throws \InvalidArgumentException for invalid DNS server address
202 | */
203 | private function createUdpExecutor($nameserver, LoopInterface $loop)
204 | {
205 | return new TimeoutExecutor(
206 | new UdpTransportExecutor(
207 | $nameserver,
208 | $loop
209 | ),
210 | 5.0,
211 | $loop
212 | );
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/Resolver/Resolver.php:
--------------------------------------------------------------------------------
1 | executor = $executor;
20 | }
21 |
22 | public function resolve($domain)
23 | {
24 | return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
25 | return $ips[array_rand($ips)];
26 | });
27 | }
28 |
29 | public function resolveAll($domain, $type)
30 | {
31 | $query = new Query($domain, $type, Message::CLASS_IN);
32 |
33 | return $this->executor->query(
34 | $query
35 | )->then(function (Message $response) use ($query) {
36 | return $this->extractValues($query, $response);
37 | });
38 | }
39 |
40 | /**
41 | * [Internal] extract all resource record values from response for this query
42 | *
43 | * @param Query $query
44 | * @param Message $response
45 | * @return array
46 | * @throws RecordNotFoundException when response indicates an error or contains no data
47 | * @internal
48 | */
49 | public function extractValues(Query $query, Message $response)
50 | {
51 | // reject if response code indicates this is an error response message
52 | $code = $response->rcode;
53 | if ($code !== Message::RCODE_OK) {
54 | switch ($code) {
55 | case Message::RCODE_FORMAT_ERROR:
56 | $message = 'Format Error';
57 | break;
58 | case Message::RCODE_SERVER_FAILURE:
59 | $message = 'Server Failure';
60 | break;
61 | case Message::RCODE_NAME_ERROR:
62 | $message = 'Non-Existent Domain / NXDOMAIN';
63 | break;
64 | case Message::RCODE_NOT_IMPLEMENTED:
65 | $message = 'Not Implemented';
66 | break;
67 | case Message::RCODE_REFUSED:
68 | $message = 'Refused';
69 | break;
70 | default:
71 | $message = 'Unknown error response code ' . $code;
72 | }
73 | throw new RecordNotFoundException(
74 | 'DNS query for ' . $query->describe() . ' returned an error response (' . $message . ')',
75 | $code
76 | );
77 | }
78 |
79 | $answers = $response->answers;
80 | $addresses = $this->valuesByNameAndType($answers, $query->name, $query->type);
81 |
82 | // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found)
83 | if (0 === count($addresses)) {
84 | throw new RecordNotFoundException(
85 | 'DNS query for ' . $query->describe() . ' did not return a valid answer (NOERROR / NODATA)'
86 | );
87 | }
88 |
89 | return array_values($addresses);
90 | }
91 |
92 | /**
93 | * @param \React\Dns\Model\Record[] $answers
94 | * @param string $name
95 | * @param int $type
96 | * @return array
97 | */
98 | private function valuesByNameAndType(array $answers, $name, $type)
99 | {
100 | // return all record values for this name and type (if any)
101 | $named = $this->filterByName($answers, $name);
102 | $records = $this->filterByType($named, $type);
103 | if ($records) {
104 | return $this->mapRecordData($records);
105 | }
106 |
107 | // no matching records found? check if there are any matching CNAMEs instead
108 | $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
109 | if ($cnameRecords) {
110 | $cnames = $this->mapRecordData($cnameRecords);
111 | foreach ($cnames as $cname) {
112 | $records = array_merge(
113 | $records,
114 | $this->valuesByNameAndType($answers, $cname, $type)
115 | );
116 | }
117 | }
118 |
119 | return $records;
120 | }
121 |
122 | private function filterByName(array $answers, $name)
123 | {
124 | return $this->filterByField($answers, 'name', $name);
125 | }
126 |
127 | private function filterByType(array $answers, $type)
128 | {
129 | return $this->filterByField($answers, 'type', $type);
130 | }
131 |
132 | private function filterByField(array $answers, $field, $value)
133 | {
134 | $value = strtolower($value);
135 | return array_filter($answers, function ($answer) use ($field, $value) {
136 | return $value === strtolower($answer->$field);
137 | });
138 | }
139 |
140 | private function mapRecordData(array $records)
141 | {
142 | return array_map(function ($record) {
143 | return $record->data;
144 | }, $records);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/Resolver/ResolverInterface.php:
--------------------------------------------------------------------------------
1 | resolve('reactphp.org')->then(function ($ip) {
12 | * echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
13 | * });
14 | * ```
15 | *
16 | * This is one of the main methods in this package. It sends a DNS query
17 | * for the given $domain name to your DNS server and returns a single IP
18 | * address on success.
19 | *
20 | * If the DNS server sends a DNS response message that contains more than
21 | * one IP address for this query, it will randomly pick one of the IP
22 | * addresses from the response. If you want the full list of IP addresses
23 | * or want to send a different type of query, you should use the
24 | * [`resolveAll()`](#resolveall) method instead.
25 | *
26 | * If the DNS server sends a DNS response message that indicates an error
27 | * code, this method will reject with a `RecordNotFoundException`. Its
28 | * message and code can be used to check for the response code.
29 | *
30 | * If the DNS communication fails and the server does not respond with a
31 | * valid response message, this message will reject with an `Exception`.
32 | *
33 | * Pending DNS queries can be cancelled by cancelling its pending promise like so:
34 | *
35 | * ```php
36 | * $promise = $resolver->resolve('reactphp.org');
37 | *
38 | * $promise->cancel();
39 | * ```
40 | *
41 | * @param string $domain
42 | * @return \React\Promise\PromiseInterface
43 | * resolves with a single IP address on success or rejects with an Exception on error.
44 | */
45 | public function resolve($domain);
46 |
47 | /**
48 | * Resolves all record values for the given $domain name and query $type.
49 | *
50 | * ```php
51 | * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
52 | * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
53 | * });
54 | *
55 | * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
56 | * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
57 | * });
58 | * ```
59 | *
60 | * This is one of the main methods in this package. It sends a DNS query
61 | * for the given $domain name to your DNS server and returns a list with all
62 | * record values on success.
63 | *
64 | * If the DNS server sends a DNS response message that contains one or more
65 | * records for this query, it will return a list with all record values
66 | * from the response. You can use the `Message::TYPE_*` constants to control
67 | * which type of query will be sent. Note that this method always returns a
68 | * list of record values, but each record value type depends on the query
69 | * type. For example, it returns the IPv4 addresses for type `A` queries,
70 | * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
71 | * `CNAME` and `PTR` queries and structured data for other queries. See also
72 | * the `Record` documentation for more details.
73 | *
74 | * If the DNS server sends a DNS response message that indicates an error
75 | * code, this method will reject with a `RecordNotFoundException`. Its
76 | * message and code can be used to check for the response code.
77 | *
78 | * If the DNS communication fails and the server does not respond with a
79 | * valid response message, this message will reject with an `Exception`.
80 | *
81 | * Pending DNS queries can be cancelled by cancelling its pending promise like so:
82 | *
83 | * ```php
84 | * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
85 | *
86 | * $promise->cancel();
87 | * ```
88 | *
89 | * @param string $domain
90 | * @return \React\Promise\PromiseInterface
91 | * Resolves with all record values on success or rejects with an Exception on error.
92 | */
93 | public function resolveAll($domain, $type);
94 | }
95 |
--------------------------------------------------------------------------------