This commit is contained in:
2024-11-27 21:34:07 +02:00
parent 638bcba894
commit b6d1215999
190 changed files with 31518 additions and 0 deletions

452
vendor/react/dns/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,452 @@
# Changelog
## 1.13.0 (2024-06-13)
* Feature: Improve PHP 8.4+ support by avoiding implicitly nullable type declarations.
(#224 by @WyriHaximus)
## 1.12.0 (2023-11-29)
* Feature: Full PHP 8.3 compatibility.
(#217 by @sergiy-petrov)
* Update test environment and avoid unhandled promise rejections.
(#215, #216 and #218 by @clue)
## 1.11.0 (2023-06-02)
* Feature: Include timeout logic to avoid dependency on reactphp/promise-timer.
(#213 by @clue)
* Improve test suite and project setup and report failed assertions.
(#210 by @clue, #212 by @WyriHaximus and #209 and #211 by @SimonFrings)
## 1.10.0 (2022-09-08)
* Feature: Full support for PHP 8.2 release.
(#201 by @clue and #207 by @WyriHaximus)
* Feature: Optimize forward compatibility with Promise v3, avoid hitting autoloader.
(#202 by @clue)
* Feature / Fix: Improve error reporting when custom error handler is used.
(#197 by @clue)
* Fix: Fix invalid references in exception stack trace.
(#191 by @clue)
* Minor documentation improvements.
(#195 by @SimonFrings and #203 by @nhedger)
* Improve test suite, update to use default loop and new reactphp/async package.
(#204, #205 and #206 by @clue and #196 by @SimonFrings)
## 1.9.0 (2021-12-20)
* Feature: Full support for PHP 8.1 release and prepare PHP 8.2 compatibility
by refactoring `Parser` to avoid assigning dynamic properties.
(#188 and #186 by @clue and #184 by @SimonFrings)
* Feature: Avoid dependency on `ext-filter`.
(#185 by @clue)
* Feature / Fix: Skip invalid nameserver entries from `resolv.conf` and ignore IPv6 zone IDs.
(#187 by @clue)
* Feature / Fix: Reduce socket read chunk size for queries over TCP/IP.
(#189 by @clue)
## 1.8.0 (2021-07-11)
A major new feature release, see [**release announcement**](https://clue.engineering/2021/announcing-reactphp-default-loop).
* Feature: Simplify usage by supporting new [default loop](https://reactphp.org/event-loop/#loop).
(#182 by @clue)
```php
// old (still supported)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->create($config, $loop);
// new (using default loop)
$factory = new React\Dns\Resolver\Factory();
$resolver = $factory->create($config);
```
## 1.7.0 (2021-06-25)
* Feature: Update DNS `Factory` to accept complete `Config` object.
Add new `FallbackExecutor` and use fallback DNS servers when `Config` lists multiple servers.
(#179 and #180 by @clue)
```php
// old (still supported)
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
$resolver = $factory->create($server, $loop);
// new
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$resolver = $factory->create($config, $loop);
```
## 1.6.0 (2021-06-21)
* Feature: Add support for legacy `SPF` record type.
(#178 by @akondas and @clue)
* Fix: Fix integer overflow for TCP/IP chunk size on 32 bit platforms.
(#177 by @clue)
## 1.5.0 (2021-03-05)
* Feature: Improve error reporting when query fails, include domain and query type and DNS server address where applicable.
(#174 by @clue)
* Feature: Improve error handling when sending data to DNS server fails (macOS).
(#171 and #172 by @clue)
* Fix: Improve DNS response parser to limit recursion for compressed labels.
(#169 by @clue)
* Improve test suite, use GitHub actions for continuous integration (CI).
(#170 by @SimonFrings)
## 1.4.0 (2020-09-18)
* Feature: Support upcoming PHP 8.
(#168 by @clue)
* Improve test suite and update to PHPUnit 9.3.
(#164 by @clue, #165 and #166 by @SimonFrings and #167 by @WyriHaximus)
## 1.3.0 (2020-07-10)
* Feature: Forward compatibility with react/promise v3.
(#153 by @WyriHaximus)
* Feature: Support parsing `OPT` records (EDNS0).
(#157 by @clue)
* Fix: Avoid PHP warnings due to lack of args in exception trace on PHP 7.4.
(#160 by @clue)
* Improve test suite and add `.gitattributes` to exclude dev files from exports.
Run tests on PHPUnit 9 and PHP 7.4 and clean up test suite.
(#154 by @reedy, #156 by @clue and #163 by @SimonFrings)
## 1.2.0 (2019-08-15)
* Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection,
add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and
automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`.
(#145, #146, #147 and #148 by @clue)
* Feature: Support escaping literal dots and special characters in domain names.
(#144 by @clue)
## 1.1.0 (2019-07-18)
* Feature: Support parsing `CAA` and `SSHFP` records.
(#141 and #142 by @clue)
* Feature: Add `ResolverInterface` as common interface for `Resolver` class.
(#139 by @clue)
* Fix: Add missing private property definitions and
remove unneeded dependency on `react/stream`.
(#140 and #143 by @clue)
## 1.0.0 (2019-07-11)
* First stable LTS release, now following [SemVer](https://semver.org/).
We'd like to emphasize that this component is production ready and battle-tested.
We plan to support all long-term support (LTS) releases for at least 24 months,
so you have a rock-solid foundation to build on top of.
This update involves a number of BC breaks due to dropped support for
deprecated functionality and some internal API cleanup. We've tried hard to
avoid BC breaks where possible and minimize impact otherwise. We expect that
most consumers of this package will actually not be affected by any BC
breaks, see below for more details:
* BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions
instead of nested arrays and increase code coverage to 100%.
(#130 by @clue)
* BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`,
remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and
add API documentation for `ExecutorInterface`.
(#135, #137 and #138 by @clue)
* BC break: Replace `HeaderBag` attributes with simple `Message` properties.
(#132 by @clue)
* BC break: Mark all `Record` attributes as required, add documentation vs `Query`.
(#136 by @clue)
* BC break: Mark all classes as final to discourage inheritance
(#134 by @WyriHaximus)
## 0.4.19 (2019-07-10)
* Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6.
(#133 by @clue)
## 0.4.18 (2019-09-07)
* Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`,
respect TTL from response records when caching and do not cache truncated responses.
(#129 by @clue)
* Feature: Limit cache size to 256 last responses by default.
(#127 by @clue)
* Feature: Cooperatively resolve hosts to avoid running same query concurrently.
(#125 by @clue)
## 0.4.17 (2019-04-01)
* Feature: Support parsing `authority` and `additional` records from DNS response.
(#123 by @clue)
* Feature: Support dumping records as part of outgoing binary DNS message.
(#124 by @clue)
* Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0
(#121 by @clue)
* Improve test suite to add forward compatibility with PHPUnit 7,
test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM.
(#122 by @clue)
## 0.4.16 (2018-11-11)
* Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references.
(#118 by @clue)
* Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages,
malformed record data or malformed compressed domain name labels.
(#115 and #117 by @clue)
* Fix: Fix interpretation of TTL as UINT32 with most significant bit unset.
(#116 by @clue)
* Fix: Fix caching advanced MX/SRV/TXT/SOA structures.
(#112 by @clue)
## 0.4.15 (2018-07-02)
* Feature: Add `resolveAll()` method to support custom query types in `Resolver`.
(#110 by @clue and @WyriHaximus)
```php
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
```
* Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
(#104, #105, #106, #107 and #108 by @clue)
* Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data.
(#104 by @clue)
* Feature: Improve error messages for failed queries and improve documentation.
(#109 by @clue)
* Feature: Add reverse DNS lookup example.
(#111 by @clue)
## 0.4.14 (2018-06-26)
* Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages
to avoid cache poisoning attacks and deprecate legacy `Executor`.
(#101 and #103 by @clue)
* Feature: Forward compatibility with Cache 0.5
(#102 by @clue)
* Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API.
(#99 by @clue)
## 0.4.13 (2018-02-27)
* Add `Config::loadSystemConfigBlocking()` to load default system config
and support parsing DNS config on all supported platforms
(`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows)
(#92, #93, #94 and #95 by @clue)
```php
$config = Config::loadSystemConfigBlocking();
$server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
```
* Remove unneeded cyclic dependency on react/socket
(#96 by @clue)
## 0.4.12 (2018-01-14)
* Improve test suite by adding forward compatibility with PHPUnit 6,
test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases,
add test group to skip integration tests relying on internet connection
and add minor documentation improvements.
(#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor)
## 0.4.11 (2017-08-25)
* Feature: Support resolving from default hosts file
(#75, #76 and #77 by @clue)
This means that resolving hosts such as `localhost` will now work as
expected across all platforms with no changes required:
```php
$resolver->resolve('localhost')->then(function ($ip) {
echo 'IP: ' . $ip;
});
```
The new `HostsExecutor` exists for advanced usage and is otherwise used
internally for this feature.
## 0.4.10 (2017-08-10)
* Feature: Forward compatibility with EventLoop v1.0 and v0.5 and
lock minimum dependencies and work around circular dependency for tests
(#70 and #71 by @clue)
* Fix: Work around DNS timeout issues for Windows users
(#74 by @clue)
* Documentation and examples for advanced usage
(#66 by @WyriHaximus)
* Remove broken TCP code, do not retry with invalid TCP query
(#73 by @clue)
* Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and
lock Travis distro so new defaults will not break the build and
fix failing tests for PHP 7.1
(#68 by @WyriHaximus and #69 and #72 by @clue)
## 0.4.9 (2017-05-01)
* Feature: Forward compatibility with upcoming Socket v1.0 and v0.8
(#61 by @clue)
## 0.4.8 (2017-04-16)
* Feature: Add support for the AAAA record type to the protocol parser
(#58 by @othillo)
* Feature: Add support for the PTR record type to the protocol parser
(#59 by @othillo)
## 0.4.7 (2017-03-31)
* Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component
(#57 by @clue)
## 0.4.6 (2017-03-11)
* Fix: Fix DNS timeout issues for Windows users and add forward compatibility
with Stream v0.5 and upcoming v0.6
(#53 by @clue)
* Improve test suite by adding PHPUnit to `require-dev`
(#54 by @clue)
## 0.4.5 (2017-03-02)
* Fix: Ensure we ignore the case of the answer
(#51 by @WyriHaximus)
* Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal
code re-use for upcoming versions.
(#48 and #49 by @clue)
## 0.4.4 (2017-02-13)
* Fix: Fix handling connection and stream errors
(#45 by @clue)
* Feature: Add examples and forward compatibility with upcoming Socket v0.5 component
(#46 and #47 by @clue)
## 0.4.3 (2016-07-31)
* Feature: Allow for cache adapter injection (#38 by @WyriHaximus)
```php
$factory = new React\Dns\Resolver\Factory();
$cache = new MyCustomCacheInstance();
$resolver = $factory->createCached('8.8.8.8', $loop, $cache);
```
* Feature: Support Promise cancellation (#35 by @clue)
```php
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();
```
## 0.4.2 (2016-02-24)
* Repository maintenance, split off from main repo, improve test suite and documentation
* First class support for PHP7 and HHVM (#34 by @clue)
* Adjust compatibility to 5.3 (#30 by @clue)
## 0.4.1 (2014-04-13)
* Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus)
## 0.4.0 (2014-02-02)
* BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks
* BC break: Update to React/Promise 2.0
* Bug fix: Properly resolve CNAME aliases
* Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0
* Bump React dependencies to v0.4
## 0.3.2 (2013-05-10)
* Feature: Support default port for IPv6 addresses (@clue)
## 0.3.0 (2013-04-14)
* Bump React dependencies to v0.3
## 0.2.6 (2012-12-26)
* Feature: New cache component, used by DNS
## 0.2.5 (2012-11-26)
* Version bump
## 0.2.4 (2012-11-18)
* Feature: Change to promise-based API (@jsor)
## 0.2.3 (2012-11-14)
* Version bump
## 0.2.2 (2012-10-28)
* Feature: DNS executor timeout handling (@arnaud-lb)
* Feature: DNS retry executor (@arnaud-lb)
## 0.2.1 (2012-10-14)
* Minor adjustments to DNS parser
## 0.2.0 (2012-09-10)
* Feature: DNS resolver

21
vendor/react/dns/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2012 Christian Lück, Cees-Jan Kiewiet, Jan Sorgalla, Chris Boden, Igor Wiedler
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

453
vendor/react/dns/README.md vendored Normal file
View File

@@ -0,0 +1,453 @@
# DNS
[![CI status](https://github.com/reactphp/dns/actions/workflows/ci.yml/badge.svg)](https://github.com/reactphp/dns/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/dns?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/dns)
Async DNS resolver for [ReactPHP](https://reactphp.org/).
The main point of the DNS component is to provide async DNS resolution.
However, it is really a toolkit for working with DNS messages, and could
easily be used to create a DNS server.
**Table of contents**
* [Basic usage](#basic-usage)
* [Caching](#caching)
* [Custom cache adapter](#custom-cache-adapter)
* [ResolverInterface](#resolverinterface)
* [resolve()](#resolve)
* [resolveAll()](#resolveall)
* [Advanced usage](#advanced-usage)
* [UdpTransportExecutor](#udptransportexecutor)
* [TcpTransportExecutor](#tcptransportexecutor)
* [SelectiveTransportExecutor](#selectivetransportexecutor)
* [HostsFileExecutor](#hostsfileexecutor)
* [Install](#install)
* [Tests](#tests)
* [License](#license)
* [References](#references)
## Basic usage
The most basic usage is to just create a resolver through the resolver
factory. All you need to give it is a nameserver, then you can start resolving
names, baby!
```php
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->create($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
```
See also the [first example](examples).
The `Config` class can be used to load the system default config. This is an
operation that may access the filesystem and block. Ideally, this method should
thus be executed only once before the loop starts and not repeatedly while it is
running.
Note that this class may return an *empty* configuration if the system config
can not be loaded. As such, you'll likely want to apply a default nameserver
as above if none can be found.
> Note that the factory loads the hosts file from the filesystem once when
creating the resolver instance.
Ideally, this method should thus be executed only once before the loop starts
and not repeatedly while it is running.
But there's more.
## Caching
You can cache results by configuring the resolver to use a `CachedExecutor`:
```php
$config = React\Dns\Config\Config::loadSystemConfigBlocking();
if (!$config->nameservers) {
$config->nameservers[] = '8.8.8.8';
}
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached($config);
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
...
$dns->resolve('igor.io')->then(function ($ip) {
echo "Host: $ip\n";
});
```
If the first call returns before the second, only one query will be executed.
The second result will be served from an in memory cache.
This is particularly useful for long running scripts where the same hostnames
have to be looked up multiple times.
See also the [third example](examples).
### Custom cache adapter
By default, the above will use an in memory cache.
You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead:
```php
$cache = new React\Cache\ArrayCache();
$factory = new React\Dns\Resolver\Factory();
$dns = $factory->createCached('8.8.8.8', null, $cache);
```
See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
## ResolverInterface
<a id="resolver"><!-- legacy reference --></a>
### resolve()
The `resolve(string $domain): PromiseInterface<string>` method can be used to
resolve the given $domain name to a single IPv4 address (type `A` query).
```php
$resolver->resolve('reactphp.org')->then(function ($ip) {
echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
});
```
This is one of the main methods in this package. It sends a DNS query
for the given $domain name to your DNS server and returns a single IP
address on success.
If the DNS server sends a DNS response message that contains more than
one IP address for this query, it will randomly pick one of the IP
addresses from the response. If you want the full list of IP addresses
or want to send a different type of query, you should use the
[`resolveAll()`](#resolveall) method instead.
If the DNS server sends a DNS response message that indicates an error
code, this method will reject with a `RecordNotFoundException`. Its
message and code can be used to check for the response code.
If the DNS communication fails and the server does not respond with a
valid response message, this message will reject with an `Exception`.
Pending DNS queries can be cancelled by cancelling its pending promise like so:
```php
$promise = $resolver->resolve('reactphp.org');
$promise->cancel();
```
### resolveAll()
The `resolveAll(string $host, int $type): PromiseInterface<array>` method can be used to
resolve all record values for the given $domain name and query $type.
```php
$resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
$resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
});
```
This is one of the main methods in this package. It sends a DNS query
for the given $domain name to your DNS server and returns a list with all
record values on success.
If the DNS server sends a DNS response message that contains one or more
records for this query, it will return a list with all record values
from the response. You can use the `Message::TYPE_*` constants to control
which type of query will be sent. Note that this method always returns a
list of record values, but each record value type depends on the query
type. For example, it returns the IPv4 addresses for type `A` queries,
the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
`CNAME` and `PTR` queries and structured data for other queries. See also
the `Record` documentation for more details.
If the DNS server sends a DNS response message that indicates an error
code, this method will reject with a `RecordNotFoundException`. Its
message and code can be used to check for the response code.
If the DNS communication fails and the server does not respond with a
valid response message, this message will reject with an `Exception`.
Pending DNS queries can be cancelled by cancelling its pending promise like so:
```php
$promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
$promise->cancel();
```
## Advanced Usage
### UdpTransportExecutor
The `UdpTransportExecutor` can be used to
send DNS queries over a UDP transport.
This is the main class that sends a DNS query to your DNS server and is used
internally by the `Resolver` for the actual message transport.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `igor.io`.
```php
$executor = new UdpTransportExecutor('8.8.8.8:53');
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
See also the [fourth example](examples).
Note that this executor does not implement a timeout, so you will very likely
want to use this in combination with a `TimeoutExecutor` like this:
```php
$executor = new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
);
```
Also note that this executor uses an unreliable UDP transport and that it
does not implement any retry logic, so you will likely want to use this in
combination with a `RetryExecutor` like this:
```php
$executor = new RetryExecutor(
new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
)
);
```
Note that this executor is entirely async and as such allows you to execute
any number of queries concurrently. You should probably limit the number of
concurrent queries in your application or you're very likely going to face
rate limitations and bans on the resolver end. For many common applications,
you may want to avoid sending the same query multiple times when the first
one is still pending, so you will likely want to use this in combination with
a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new RetryExecutor(
new TimeoutExecutor(
new UdpTransportExecutor($nameserver),
3.0
)
)
);
```
> Internally, this class uses PHP's UDP sockets and does not take advantage
of [react/datagram](https://github.com/reactphp/datagram) purely for
organizational reasons to avoid a cyclic dependency between the two
packages. Higher-level components should take advantage of the Datagram
component instead of reimplementing this socket logic from scratch.
### TcpTransportExecutor
The `TcpTransportExecutor` class can be used to
send DNS queries over a TCP/IP stream transport.
This is one of the main classes that send a DNS query to your DNS server.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `reactphp.org`.
```php
$executor = new TcpTransportExecutor('8.8.8.8:53');
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
See also [example #92](examples).
Note that this executor does not implement a timeout, so you will very likely
want to use this in combination with a `TimeoutExecutor` like this:
```php
$executor = new TimeoutExecutor(
new TcpTransportExecutor($nameserver),
3.0
);
```
Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
transport, so you do not necessarily have to implement any retry logic.
Note that this executor is entirely async and as such allows you to execute
queries concurrently. The first query will establish a TCP/IP socket
connection to the DNS server which will be kept open for a short period.
Additional queries will automatically reuse this existing socket connection
to the DNS server, will pipeline multiple requests over this single
connection and will keep an idle connection open for a short period. The
initial TCP/IP connection overhead may incur a slight delay if you only send
occasional queries when sending a larger number of concurrent queries over
an existing connection, it becomes increasingly more efficient and avoids
creating many concurrent sockets like the UDP-based executor. You may still
want to limit the number of (concurrent) queries in your application or you
may be facing rate limitations and bans on the resolver end. For many common
applications, you may want to avoid sending the same query multiple times
when the first one is still pending, so you will likely want to use this in
combination with a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new TimeoutExecutor(
new TcpTransportExecutor($nameserver),
3.0
)
);
```
> Internally, this class uses PHP's TCP/IP sockets and does not take advantage
of [react/socket](https://github.com/reactphp/socket) purely for
organizational reasons to avoid a cyclic dependency between the two
packages. Higher-level components should take advantage of the Socket
component instead of reimplementing this socket logic from scratch.
### SelectiveTransportExecutor
The `SelectiveTransportExecutor` class can be used to
Send DNS queries over a UDP or TCP/IP stream transport.
This class will automatically choose the correct transport protocol to send
a DNS query to your DNS server. It will always try to send it over the more
efficient UDP transport first. If this query yields a size related issue
(truncated messages), it will retry over a streaming TCP/IP transport.
For more advanced usages one can utilize this class directly.
The following example looks up the `IPv6` address for `reactphp.org`.
```php
$executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
$executor->query(
new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
)->then(function (Message $message) {
foreach ($message->answers as $answer) {
echo 'IPv6: ' . $answer->data . PHP_EOL;
}
}, 'printf');
```
Note that this executor only implements the logic to select the correct
transport for the given DNS query. Implementing the correct transport logic,
implementing timeouts and any retry logic is left up to the given executors,
see also [`UdpTransportExecutor`](#udptransportexecutor) and
[`TcpTransportExecutor`](#tcptransportexecutor) for more details.
Note that this executor is entirely async and as such allows you to execute
any number of queries concurrently. You should probably limit the number of
concurrent queries in your application or you're very likely going to face
rate limitations and bans on the resolver end. For many common applications,
you may want to avoid sending the same query multiple times when the first
one is still pending, so you will likely want to use this in combination with
a `CoopExecutor` like this:
```php
$executor = new CoopExecutor(
new SelectiveTransportExecutor(
$datagramExecutor,
$streamExecutor
)
);
```
### HostsFileExecutor
Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
If you also want to take entries from your hosts file into account, you may
use this code:
```php
$hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
$executor = new UdpTransportExecutor('8.8.8.8:53');
$executor = new HostsFileExecutor($hosts, $executor);
$executor->query(
new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
);
```
## Install
The recommended way to install this library is [through Composer](https://getcomposer.org/).
[New to Composer?](https://getcomposer.org/doc/00-intro.md)
This project follows [SemVer](https://semver.org/).
This will install the latest supported version:
```bash
composer require react/dns:^1.13
```
See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
This project aims to run on any platform and thus does not require any PHP
extensions and supports running on legacy PHP 5.3 through current PHP 8+ and
HHVM.
It's *highly recommended to use the latest supported PHP version* for this project.
## Tests
To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](https://getcomposer.org/):
```bash
composer install
```
To run the test suite, go to the project root and run:
```bash
vendor/bin/phpunit
```
The test suite also contains a number of functional integration tests that rely
on a stable internet connection.
If you do not want to run these, they can simply be skipped like this:
```bash
vendor/bin/phpunit --exclude-group internet
```
## License
MIT, see [LICENSE file](LICENSE).
## References
* [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities
* [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification

49
vendor/react/dns/composer.json vendored Normal file
View File

@@ -0,0 +1,49 @@
{
"name": "react/dns",
"description": "Async DNS resolver for ReactPHP",
"keywords": ["dns", "dns-resolver", "ReactPHP", "async"],
"license": "MIT",
"authors": [
{
"name": "Christian Lück",
"homepage": "https://clue.engineering/",
"email": "christian@clue.engineering"
},
{
"name": "Cees-Jan Kiewiet",
"homepage": "https://wyrihaximus.net/",
"email": "reactphp@ceesjankiewiet.nl"
},
{
"name": "Jan Sorgalla",
"homepage": "https://sorgalla.com/",
"email": "jsorgalla@gmail.com"
},
{
"name": "Chris Boden",
"homepage": "https://cboden.dev/",
"email": "cboden@gmail.com"
}
],
"require": {
"php": ">=5.3.0",
"react/cache": "^1.0 || ^0.6 || ^0.5",
"react/event-loop": "^1.2",
"react/promise": "^3.2 || ^2.7 || ^1.2.1"
},
"require-dev": {
"phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36",
"react/async": "^4.3 || ^3 || ^2",
"react/promise-timer": "^1.11"
},
"autoload": {
"psr-4": {
"React\\Dns\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"React\\Tests\\Dns\\": "tests/"
}
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns;
final class BadServerException extends \Exception
{
}

137
vendor/react/dns/src/Config/Config.php vendored Normal file
View File

@@ -0,0 +1,137 @@
<?php
namespace React\Dns\Config;
use RuntimeException;
final class Config
{
/**
* Loads the system DNS configuration
*
* Note that this method may block while loading its internal files and/or
* commands and should thus be used with care! While this should be
* relatively fast for most systems, it remains unknown if this may block
* under certain circumstances. In particular, this method should only be
* executed before the loop starts, not while it is running.
*
* Note that this method will try to access its files and/or commands and
* try to parse its output. Currently, this will only parse valid nameserver
* entries from its output and will ignore all other output without
* complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @return self
* @codeCoverageIgnore
*/
public static function loadSystemConfigBlocking()
{
// Use WMIC output on Windows
if (DIRECTORY_SEPARATOR === '\\') {
return self::loadWmicBlocking();
}
// otherwise (try to) load from resolv.conf
try {
return self::loadResolvConfBlocking();
} catch (RuntimeException $ignored) {
// return empty config if parsing fails (file not found)
return new self();
}
}
/**
* Loads a resolv.conf file (from the given path or default location)
*
* Note that this method blocks while loading the given path and should
* thus be used with care! While this should be relatively fast for normal
* resolv.conf files, this may be an issue if this file is located on a slow
* device or contains an excessive number of entries. In particular, this
* method should only be executed before the loop starts, not while it is
* running.
*
* Note that this method will throw if the given file can not be loaded,
* such as if it is not readable or does not exist. In particular, this file
* is not available on Windows.
*
* Currently, this will only parse valid "nameserver X" lines from the
* given file contents. Lines can be commented out with "#" and ";" and
* invalid lines will be ignored without complaining. See also
* `man resolv.conf` for more details.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid "nameserver X" lines can be found. See also
* `man resolv.conf` which suggests that the DNS server on the localhost
* should be used in this case. This is left up to higher level consumers
* of this API.
*
* @param ?string $path (optional) path to resolv.conf file or null=load default location
* @return self
* @throws RuntimeException if the path can not be loaded (does not exist)
*/
public static function loadResolvConfBlocking($path = null)
{
if ($path === null) {
$path = '/etc/resolv.conf';
}
$contents = @file_get_contents($path);
if ($contents === false) {
throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"');
}
$matches = array();
preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches);
$config = new self();
foreach ($matches[1] as $ip) {
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
$ip = substr($ip, 0, $pos);
}
if (@inet_pton($ip) !== false) {
$config->nameservers[] = $ip;
}
}
return $config;
}
/**
* Loads the DNS configurations from Windows's WMIC (from the given command or default command)
*
* Note that this method blocks while loading the given command and should
* thus be used with care! While this should be relatively fast for normal
* WMIC commands, it remains unknown if this may block under certain
* circumstances. In particular, this method should only be executed before
* the loop starts, not while it is running.
*
* Note that this method will only try to execute the given command try to
* parse its output, irrespective of whether this command exists. In
* particular, this command is only available on Windows. Currently, this
* will only parse valid nameserver entries from the command output and will
* ignore all other output without complaining.
*
* Note that the previous section implies that this may return an empty
* `Config` object if no valid nameserver entries can be found.
*
* @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing
* @return self
* @link https://ss64.com/nt/wmic.html
*/
public static function loadWmicBlocking($command = null)
{
$contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command);
preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches);
$config = new self();
$config->nameservers = $matches[1];
return $config;
}
public $nameservers = array();
}

View File

@@ -0,0 +1,153 @@
<?php
namespace React\Dns\Config;
use RuntimeException;
/**
* Represents a static hosts file which maps hostnames to IPs
*
* Hosts files are used on most systems to avoid actually hitting the DNS for
* certain common hostnames.
*
* Most notably, this file usually contains an entry to map "localhost" to the
* local IP. Windows is a notable exception here, as Windows does not actually
* include "localhost" in this file by default. To compensate for this, this
* class may explicitly be wrapped in another HostsFile instance which
* hard-codes these entries for Windows (see also Factory).
*
* This class mostly exists to abstract the parsing/extraction process so this
* can be replaced with a faster alternative in the future.
*/
class HostsFile
{
/**
* Returns the default path for the hosts file on this system
*
* @return string
* @codeCoverageIgnore
*/
public static function getDefaultPath()
{
// use static path for all Unix-based systems
if (DIRECTORY_SEPARATOR !== '\\') {
return '/etc/hosts';
}
// Windows actually stores the path in the registry under
// \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath
$path = '%SystemRoot%\\system32\drivers\etc\hosts';
$base = getenv('SystemRoot');
if ($base === false) {
$base = 'C:\\Windows';
}
return str_replace('%SystemRoot%', $base, $path);
}
/**
* Loads a hosts file (from the given path or default location)
*
* Note that this method blocks while loading the given path and should
* thus be used with care! While this should be relatively fast for normal
* hosts file, this may be an issue if this file is located on a slow device
* or contains an excessive number of entries. In particular, this method
* should only be executed before the loop starts, not while it is running.
*
* @param ?string $path (optional) path to hosts file or null=load default location
* @return self
* @throws RuntimeException if the path can not be loaded (does not exist)
*/
public static function loadFromPathBlocking($path = null)
{
if ($path === null) {
$path = self::getDefaultPath();
}
$contents = @file_get_contents($path);
if ($contents === false) {
throw new RuntimeException('Unable to load hosts file "' . $path . '"');
}
return new self($contents);
}
private $contents;
/**
* Instantiate new hosts file with the given hosts file contents
*
* @param string $contents
*/
public function __construct($contents)
{
// remove all comments from the contents
$contents = preg_replace('/[ \t]*#.*/', '', strtolower($contents));
$this->contents = $contents;
}
/**
* Returns all IPs for the given hostname
*
* @param string $name
* @return string[]
*/
public function getIpsForHost($name)
{
$name = strtolower($name);
$ips = array();
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
$parts = preg_split('/\s+/', $line);
$ip = array_shift($parts);
if ($parts && array_search($name, $parts) !== false) {
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) {
$ip = substr($ip, 0, $pos);
}
if (@inet_pton($ip) !== false) {
$ips[] = $ip;
}
}
}
return $ips;
}
/**
* Returns all hostnames for the given IPv4 or IPv6 address
*
* @param string $ip
* @return string[]
*/
public function getHostsForIp($ip)
{
// check binary representation of IP to avoid string case and short notation
$ip = @inet_pton($ip);
if ($ip === false) {
return array();
}
$names = array();
foreach (preg_split('/\r?\n/', $this->contents) as $line) {
$parts = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY);
$addr = (string) array_shift($parts);
// remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`)
if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) {
$addr = substr($addr, 0, $pos);
}
if (@inet_pton($addr) === $ip) {
foreach ($parts as $part) {
$names[] = $part;
}
}
}
return $names;
}
}

230
vendor/react/dns/src/Model/Message.php vendored Normal file
View File

@@ -0,0 +1,230 @@
<?php
namespace React\Dns\Model;
use React\Dns\Query\Query;
/**
* This class represents an outgoing query message or an incoming response message
*
* @link https://tools.ietf.org/html/rfc1035#section-4.1.1
*/
final class Message
{
const TYPE_A = 1;
const TYPE_NS = 2;
const TYPE_CNAME = 5;
const TYPE_SOA = 6;
const TYPE_PTR = 12;
const TYPE_MX = 15;
const TYPE_TXT = 16;
const TYPE_AAAA = 28;
const TYPE_SRV = 33;
const TYPE_SSHFP = 44;
/**
* pseudo-type for EDNS0
*
* These are included in the additional section and usually not in answer section.
* Defined in [RFC 6891](https://tools.ietf.org/html/rfc6891) (or older
* [RFC 2671](https://tools.ietf.org/html/rfc2671)).
*
* The OPT record uses the "class" field to store the maximum size.
*
* The OPT record uses the "ttl" field to store additional flags.
*/
const TYPE_OPT = 41;
/**
* Sender Policy Framework (SPF) had a dedicated SPF type which has been
* deprecated in favor of reusing the existing TXT type.
*
* @deprecated https://datatracker.ietf.org/doc/html/rfc7208#section-3.1
* @see self::TYPE_TXT
*/
const TYPE_SPF = 99;
const TYPE_ANY = 255;
const TYPE_CAA = 257;
const CLASS_IN = 1;
const OPCODE_QUERY = 0;
const OPCODE_IQUERY = 1; // inverse query
const OPCODE_STATUS = 2;
const RCODE_OK = 0;
const RCODE_FORMAT_ERROR = 1;
const RCODE_SERVER_FAILURE = 2;
const RCODE_NAME_ERROR = 3;
const RCODE_NOT_IMPLEMENTED = 4;
const RCODE_REFUSED = 5;
/**
* The edns-tcp-keepalive EDNS0 Option
*
* Option value contains a `?float` with timeout in seconds (in 0.1s steps)
* for DNS response or `null` for DNS query.
*
* @link https://tools.ietf.org/html/rfc7828
*/
const OPT_TCP_KEEPALIVE = 11;
/**
* The EDNS(0) Padding Option
*
* Option value contains a `string` with binary data (usually variable
* number of null bytes)
*
* @link https://tools.ietf.org/html/rfc7830
*/
const OPT_PADDING = 12;
/**
* Creates a new request message for the given query
*
* @param Query $query
* @return self
*/
public static function createRequestForQuery(Query $query)
{
$request = new Message();
$request->id = self::generateId();
$request->rd = true;
$request->questions[] = $query;
return $request;
}
/**
* Creates a new response message for the given query with the given answer records
*
* @param Query $query
* @param Record[] $answers
* @return self
*/
public static function createResponseWithAnswersForQuery(Query $query, array $answers)
{
$response = new Message();
$response->id = self::generateId();
$response->qr = true;
$response->rd = true;
$response->questions[] = $query;
foreach ($answers as $record) {
$response->answers[] = $record;
}
return $response;
}
/**
* generates a random 16 bit message ID
*
* This uses a CSPRNG so that an outside attacker that is sending spoofed
* DNS response messages can not guess the message ID to avoid possible
* cache poisoning attacks.
*
* The `random_int()` function is only available on PHP 7+ or when
* https://github.com/paragonie/random_compat is installed. As such, using
* the latest supported PHP version is highly recommended. This currently
* falls back to a less secure random number generator on older PHP versions
* in the hope that this system is properly protected against outside
* attackers, for example by using one of the common local DNS proxy stubs.
*
* @return int
* @see self::getId()
* @codeCoverageIgnore
*/
private static function generateId()
{
if (function_exists('random_int')) {
return random_int(0, 0xffff);
}
return mt_rand(0, 0xffff);
}
/**
* The 16 bit message ID
*
* The response message ID has to match the request message ID. This allows
* the receiver to verify this is the correct response message. An outside
* attacker may try to inject fake responses by "guessing" the message ID,
* so this should use a proper CSPRNG to avoid possible cache poisoning.
*
* @var int 16 bit message ID
* @see self::generateId()
*/
public $id = 0;
/**
* @var bool Query/Response flag, query=false or response=true
*/
public $qr = false;
/**
* @var int specifies the kind of query (4 bit), see self::OPCODE_* constants
* @see self::OPCODE_QUERY
*/
public $opcode = self::OPCODE_QUERY;
/**
*
* @var bool Authoritative Answer
*/
public $aa = false;
/**
* @var bool TrunCation
*/
public $tc = false;
/**
* @var bool Recursion Desired
*/
public $rd = false;
/**
* @var bool Recursion Available
*/
public $ra = false;
/**
* @var int response code (4 bit), see self::RCODE_* constants
* @see self::RCODE_OK
*/
public $rcode = Message::RCODE_OK;
/**
* An array of Query objects
*
* ```php
* $questions = array(
* new Query(
* 'reactphp.org',
* Message::TYPE_A,
* Message::CLASS_IN
* )
* );
* ```
*
* @var Query[]
*/
public $questions = array();
/**
* @var Record[]
*/
public $answers = array();
/**
* @var Record[]
*/
public $authority = array();
/**
* @var Record[]
*/
public $additional = array();
}

153
vendor/react/dns/src/Model/Record.php vendored Normal file
View File

@@ -0,0 +1,153 @@
<?php
namespace React\Dns\Model;
/**
* This class represents a single resulting record in a response message
*
* It uses a structure similar to `\React\Dns\Query\Query`, but does include
* fields for resulting TTL and resulting record data (IPs etc.).
*
* @link https://tools.ietf.org/html/rfc1035#section-4.1.3
* @see \React\Dns\Query\Query
*/
final class Record
{
/**
* @var string hostname without trailing dot, for example "reactphp.org"
*/
public $name;
/**
* @var int see Message::TYPE_* constants (UINT16)
*/
public $type;
/**
* Defines the network class, usually `Message::CLASS_IN`.
*
* For `OPT` records (EDNS0), this defines the maximum message size instead.
*
* @var int see Message::CLASS_IN constant (UINT16)
* @see Message::CLASS_IN
*/
public $class;
/**
* Defines the maximum time-to-live (TTL) in seconds
*
* For `OPT` records (EDNS0), this defines additional flags instead.
*
* @var int maximum TTL in seconds (UINT32, most significant bit always unset)
* @link https://tools.ietf.org/html/rfc2181#section-8
* @link https://tools.ietf.org/html/rfc6891#section-6.1.3 for `OPT` records (EDNS0)
*/
public $ttl;
/**
* The payload data for this record
*
* The payload data format depends on the record type. As a rule of thumb,
* this library will try to express this in a way that can be consumed
* easily without having to worry about DNS internals and its binary transport:
*
* - A:
* IPv4 address string, for example "192.168.1.1".
*
* - AAAA:
* IPv6 address string, for example "::1".
*
* - CNAME / PTR / NS:
* The hostname without trailing dot, for example "reactphp.org".
*
* - TXT:
* List of string values, for example `["v=spf1 include:example.com"]`.
* This is commonly a list with only a single string value, but this
* technically allows multiple strings (0-255 bytes each) in a single
* record. This is rarely used and depending on application you may want
* to join these together or handle them separately. Each string can
* transport any binary data, its character encoding is not defined (often
* ASCII/UTF-8 in practice). [RFC 1464](https://tools.ietf.org/html/rfc1464)
* suggests using key-value pairs such as `["name=test","version=1"]`, but
* interpretation of this is not enforced and left up to consumers of this
* library (used for DNS-SD/Zeroconf and others).
*
* - MX:
* Mail server priority (UINT16) and target hostname without trailing dot,
* for example `{"priority":10,"target":"mx.example.com"}`.
* The payload data uses an associative array with fixed keys "priority"
* (also commonly referred to as weight or preference) and "target" (also
* referred to as exchange). If a response message contains multiple
* records of this type, targets should be sorted by priority (lowest
* first) - this is left up to consumers of this library (used for SMTP).
*
* - SRV:
* Service priority (UINT16), service weight (UINT16), service port (UINT16)
* and target hostname without trailing dot, for example
* `{"priority":10,"weight":50,"port":8080,"target":"example.com"}`.
* The payload data uses an associative array with fixed keys "priority",
* "weight", "port" and "target" (also referred to as name).
* The target may be an empty host name string if the service is decidedly
* not available. If a response message contains multiple records of this
* type, targets should be sorted by priority (lowest first) and selected
* randomly according to their weight - this is left up to consumers of
* this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782)
* for more details.
*
* - SSHFP:
* Includes algorithm (UNIT8), fingerprint type (UNIT8) and fingerprint
* value as lower case hex string, for example:
* `{"algorithm":1,"type":1,"fingerprint":"0123456789abcdef..."}`
* See also https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml
* for algorithm and fingerprint type assignments.
*
* - SOA:
* Includes master hostname without trailing dot, responsible person email
* as hostname without trailing dot and serial, refresh, retry, expire and
* minimum times in seconds (UINT32 each), for example:
* `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial":
* 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`.
*
* - CAA:
* Includes flag (UNIT8), tag string and value string, for example:
* `{"flag":128,"tag":"issue","value":"letsencrypt.org"}`
*
* - OPT:
* Special pseudo-type for EDNS0. Includes an array of additional opt codes
* with a value according to the respective OPT code. See `Message::OPT_*`
* for list of supported OPT codes. Any other OPT code not currently
* supported will be an opaque binary string containing the raw data
* as transported in the DNS record. For forwards compatibility, you should
* not rely on this format for unknown types. Future versions may add
* support for new types and this may then parse the payload data
* appropriately - this will not be considered a BC break. See also
* [RFC 6891](https://tools.ietf.org/html/rfc6891) for more details.
*
* - Any other unknown type:
* An opaque binary string containing the RDATA as transported in the DNS
* record. For forwards compatibility, you should not rely on this format
* for unknown types. Future versions may add support for new types and
* this may then parse the payload data appropriately - this will not be
* considered a BC break. See the format definition of known types above
* for more details.
*
* @var string|string[]|array
*/
public $data;
/**
* @param string $name
* @param int $type
* @param int $class
* @param int $ttl
* @param string|string[]|array $data
*/
public function __construct($name, $type, $class, $ttl, $data)
{
$this->name = $name;
$this->type = $type;
$this->class = $class;
$this->ttl = $ttl;
$this->data = $data;
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace React\Dns\Protocol;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Dns\Query\Query;
final class BinaryDumper
{
/**
* @param Message $message
* @return string
*/
public function toBinary(Message $message)
{
$data = '';
$data .= $this->headerToBinary($message);
$data .= $this->questionToBinary($message->questions);
$data .= $this->recordsToBinary($message->answers);
$data .= $this->recordsToBinary($message->authority);
$data .= $this->recordsToBinary($message->additional);
return $data;
}
/**
* @param Message $message
* @return string
*/
private function headerToBinary(Message $message)
{
$data = '';
$data .= pack('n', $message->id);
$flags = 0x00;
$flags = ($flags << 1) | ($message->qr ? 1 : 0);
$flags = ($flags << 4) | $message->opcode;
$flags = ($flags << 1) | ($message->aa ? 1 : 0);
$flags = ($flags << 1) | ($message->tc ? 1 : 0);
$flags = ($flags << 1) | ($message->rd ? 1 : 0);
$flags = ($flags << 1) | ($message->ra ? 1 : 0);
$flags = ($flags << 3) | 0; // skip unused zero bit
$flags = ($flags << 4) | $message->rcode;
$data .= pack('n', $flags);
$data .= pack('n', count($message->questions));
$data .= pack('n', count($message->answers));
$data .= pack('n', count($message->authority));
$data .= pack('n', count($message->additional));
return $data;
}
/**
* @param Query[] $questions
* @return string
*/
private function questionToBinary(array $questions)
{
$data = '';
foreach ($questions as $question) {
$data .= $this->domainNameToBinary($question->name);
$data .= pack('n*', $question->type, $question->class);
}
return $data;
}
/**
* @param Record[] $records
* @return string
*/
private function recordsToBinary(array $records)
{
$data = '';
foreach ($records as $record) {
/* @var $record Record */
switch ($record->type) {
case Message::TYPE_A:
case Message::TYPE_AAAA:
$binary = \inet_pton($record->data);
break;
case Message::TYPE_CNAME:
case Message::TYPE_NS:
case Message::TYPE_PTR:
$binary = $this->domainNameToBinary($record->data);
break;
case Message::TYPE_TXT:
case Message::TYPE_SPF:
$binary = $this->textsToBinary($record->data);
break;
case Message::TYPE_MX:
$binary = \pack(
'n',
$record->data['priority']
);
$binary .= $this->domainNameToBinary($record->data['target']);
break;
case Message::TYPE_SRV:
$binary = \pack(
'n*',
$record->data['priority'],
$record->data['weight'],
$record->data['port']
);
$binary .= $this->domainNameToBinary($record->data['target']);
break;
case Message::TYPE_SOA:
$binary = $this->domainNameToBinary($record->data['mname']);
$binary .= $this->domainNameToBinary($record->data['rname']);
$binary .= \pack(
'N*',
$record->data['serial'],
$record->data['refresh'],
$record->data['retry'],
$record->data['expire'],
$record->data['minimum']
);
break;
case Message::TYPE_CAA:
$binary = \pack(
'C*',
$record->data['flag'],
\strlen($record->data['tag'])
);
$binary .= $record->data['tag'];
$binary .= $record->data['value'];
break;
case Message::TYPE_SSHFP:
$binary = \pack(
'CCH*',
$record->data['algorithm'],
$record->data['type'],
$record->data['fingerprint']
);
break;
case Message::TYPE_OPT:
$binary = '';
foreach ($record->data as $opt => $value) {
if ($opt === Message::OPT_TCP_KEEPALIVE && $value !== null) {
$value = \pack('n', round($value * 10));
}
$binary .= \pack('n*', $opt, \strlen((string) $value)) . $value;
}
break;
default:
// RDATA is already stored as binary value for unknown record types
$binary = $record->data;
}
$data .= $this->domainNameToBinary($record->name);
$data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary));
$data .= $binary;
}
return $data;
}
/**
* @param string[] $texts
* @return string
*/
private function textsToBinary(array $texts)
{
$data = '';
foreach ($texts as $text) {
$data .= \chr(\strlen($text)) . $text;
}
return $data;
}
/**
* @param string $host
* @return string
*/
private function domainNameToBinary($host)
{
if ($host === '') {
return "\0";
}
// break up domain name at each dot that is not preceeded by a backslash (escaped notation)
return $this->textsToBinary(
\array_map(
'stripcslashes',
\preg_split(
'/(?<!\\\\)\./',
$host . '.'
)
)
);
}
}

356
vendor/react/dns/src/Protocol/Parser.php vendored Normal file
View File

@@ -0,0 +1,356 @@
<?php
namespace React\Dns\Protocol;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Dns\Query\Query;
use InvalidArgumentException;
/**
* DNS protocol parser
*
* Obsolete and uncommon types and classes are not implemented.
*/
final class Parser
{
/**
* Parses the given raw binary message into a Message object
*
* @param string $data
* @throws InvalidArgumentException
* @return Message
*/
public function parseMessage($data)
{
$message = $this->parse($data, 0);
if ($message === null) {
throw new InvalidArgumentException('Unable to parse binary message');
}
return $message;
}
/**
* @param string $data
* @param int $consumed
* @return ?Message
*/
private function parse($data, $consumed)
{
if (!isset($data[12 - 1])) {
return null;
}
list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($data, 0, 12)));
$message = new Message();
$message->id = $id;
$message->rcode = $fields & 0xf;
$message->ra = (($fields >> 7) & 1) === 1;
$message->rd = (($fields >> 8) & 1) === 1;
$message->tc = (($fields >> 9) & 1) === 1;
$message->aa = (($fields >> 10) & 1) === 1;
$message->opcode = ($fields >> 11) & 0xf;
$message->qr = (($fields >> 15) & 1) === 1;
$consumed += 12;
// parse all questions
for ($i = $qdCount; $i > 0; --$i) {
list($question, $consumed) = $this->parseQuestion($data, $consumed);
if ($question === null) {
return null;
} else {
$message->questions[] = $question;
}
}
// parse all answer records
for ($i = $anCount; $i > 0; --$i) {
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return null;
} else {
$message->answers[] = $record;
}
}
// parse all authority records
for ($i = $nsCount; $i > 0; --$i) {
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return null;
} else {
$message->authority[] = $record;
}
}
// parse all additional records
for ($i = $arCount; $i > 0; --$i) {
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return null;
} else {
$message->additional[] = $record;
}
}
return $message;
}
/**
* @param string $data
* @param int $consumed
* @return array
*/
private function parseQuestion($data, $consumed)
{
list($labels, $consumed) = $this->readLabels($data, $consumed);
if ($labels === null || !isset($data[$consumed + 4 - 1])) {
return array(null, null);
}
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
$consumed += 4;
return array(
new Query(
implode('.', $labels),
$type,
$class
),
$consumed
);
}
/**
* @param string $data
* @param int $consumed
* @return array An array with a parsed Record on success or array with null if data is invalid/incomplete
*/
private function parseRecord($data, $consumed)
{
list($name, $consumed) = $this->readDomain($data, $consumed);
if ($name === null || !isset($data[$consumed + 10 - 1])) {
return array(null, null);
}
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
$consumed += 4;
list($ttl) = array_values(unpack('N', substr($data, $consumed, 4)));
$consumed += 4;
// TTL is a UINT32 that must not have most significant bit set for BC reasons
if ($ttl < 0 || $ttl >= 1 << 31) {
$ttl = 0;
}
list($rdLength) = array_values(unpack('n', substr($data, $consumed, 2)));
$consumed += 2;
if (!isset($data[$consumed + $rdLength - 1])) {
return array(null, null);
}
$rdata = null;
$expected = $consumed + $rdLength;
if (Message::TYPE_A === $type) {
if ($rdLength === 4) {
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
$consumed += $rdLength;
}
} elseif (Message::TYPE_AAAA === $type) {
if ($rdLength === 16) {
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
$consumed += $rdLength;
}
} elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
list($rdata, $consumed) = $this->readDomain($data, $consumed);
} elseif (Message::TYPE_TXT === $type || Message::TYPE_SPF === $type) {
$rdata = array();
while ($consumed < $expected) {
$len = ord($data[$consumed]);
$rdata[] = (string)substr($data, $consumed + 1, $len);
$consumed += $len + 1;
}
} elseif (Message::TYPE_MX === $type) {
if ($rdLength > 2) {
list($priority) = array_values(unpack('n', substr($data, $consumed, 2)));
list($target, $consumed) = $this->readDomain($data, $consumed + 2);
$rdata = array(
'priority' => $priority,
'target' => $target
);
}
} elseif (Message::TYPE_SRV === $type) {
if ($rdLength > 6) {
list($priority, $weight, $port) = array_values(unpack('n*', substr($data, $consumed, 6)));
list($target, $consumed) = $this->readDomain($data, $consumed + 6);
$rdata = array(
'priority' => $priority,
'weight' => $weight,
'port' => $port,
'target' => $target
);
}
} elseif (Message::TYPE_SSHFP === $type) {
if ($rdLength > 2) {
list($algorithm, $hash) = \array_values(\unpack('C*', \substr($data, $consumed, 2)));
$fingerprint = \bin2hex(\substr($data, $consumed + 2, $rdLength - 2));
$consumed += $rdLength;
$rdata = array(
'algorithm' => $algorithm,
'type' => $hash,
'fingerprint' => $fingerprint
);
}
} elseif (Message::TYPE_SOA === $type) {
list($mname, $consumed) = $this->readDomain($data, $consumed);
list($rname, $consumed) = $this->readDomain($data, $consumed);
if ($mname !== null && $rname !== null && isset($data[$consumed + 20 - 1])) {
list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($data, $consumed, 20)));
$consumed += 20;
$rdata = array(
'mname' => $mname,
'rname' => $rname,
'serial' => $serial,
'refresh' => $refresh,
'retry' => $retry,
'expire' => $expire,
'minimum' => $minimum
);
}
} elseif (Message::TYPE_OPT === $type) {
$rdata = array();
while (isset($data[$consumed + 4 - 1])) {
list($code, $length) = array_values(unpack('n*', substr($data, $consumed, 4)));
$value = (string) substr($data, $consumed + 4, $length);
if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') {
$value = null;
} elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) {
list($value) = array_values(unpack('n', $value));
$value = round($value * 0.1, 1);
} elseif ($code === Message::OPT_TCP_KEEPALIVE) {
break;
}
$rdata[$code] = $value;
$consumed += 4 + $length;
}
} elseif (Message::TYPE_CAA === $type) {
if ($rdLength > 3) {
list($flag, $tagLength) = array_values(unpack('C*', substr($data, $consumed, 2)));
if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) {
$tag = substr($data, $consumed + 2, $tagLength);
$value = substr($data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
$consumed += $rdLength;
$rdata = array(
'flag' => $flag,
'tag' => $tag,
'value' => $value
);
}
}
} else {
// unknown types simply parse rdata as an opaque binary string
$rdata = substr($data, $consumed, $rdLength);
$consumed += $rdLength;
}
// ensure parsing record data consumes expact number of bytes indicated in record length
if ($consumed !== $expected || $rdata === null) {
return array(null, null);
}
return array(
new Record($name, $type, $class, $ttl, $rdata),
$consumed
);
}
private function readDomain($data, $consumed)
{
list ($labels, $consumed) = $this->readLabels($data, $consumed);
if ($labels === null) {
return array(null, null);
}
// use escaped notation for each label part, then join using dots
return array(
\implode(
'.',
\array_map(
function ($label) {
return \addcslashes($label, "\0..\40.\177");
},
$labels
)
),
$consumed
);
}
/**
* @param string $data
* @param int $consumed
* @param int $compressionDepth maximum depth for compressed labels to avoid unreasonable recursion
* @return array
*/
private function readLabels($data, $consumed, $compressionDepth = 127)
{
$labels = array();
while (true) {
if (!isset($data[$consumed])) {
return array(null, null);
}
$length = \ord($data[$consumed]);
// end of labels reached
if ($length === 0) {
$consumed += 1;
break;
}
// first two bits set? this is a compressed label (14 bit pointer offset)
if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1]) && $compressionDepth) {
$offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]);
if ($offset >= $consumed) {
return array(null, null);
}
$consumed += 2;
list($newLabels) = $this->readLabels($data, $offset, $compressionDepth - 1);
if ($newLabels === null) {
return array(null, null);
}
$labels = array_merge($labels, $newLabels);
break;
}
// length MUST be 0-63 (6 bits only) and data has to be large enough
if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) {
return array(null, null);
}
$labels[] = substr($data, $consumed + 1, $length);
$consumed += $length + 1;
}
return array($labels, $consumed);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace React\Dns\Query;
use React\Cache\CacheInterface;
use React\Dns\Model\Message;
use React\Promise\Promise;
final class CachingExecutor implements ExecutorInterface
{
/**
* Default TTL for negative responses (NXDOMAIN etc.).
*
* @internal
*/
const TTL = 60;
private $executor;
private $cache;
public function __construct(ExecutorInterface $executor, CacheInterface $cache)
{
$this->executor = $executor;
$this->cache = $cache;
}
public function query(Query $query)
{
$id = $query->name . ':' . $query->type . ':' . $query->class;
$cache = $this->cache;
$that = $this;
$executor = $this->executor;
$pending = $cache->get($id);
return new Promise(function ($resolve, $reject) use ($query, $id, $cache, $executor, &$pending, $that) {
$pending->then(
function ($message) use ($query, $id, $cache, $executor, &$pending, $that) {
// return cached response message on cache hit
if ($message !== null) {
return $message;
}
// perform DNS lookup if not already cached
return $pending = $executor->query($query)->then(
function (Message $message) use ($cache, $id, $that) {
// DNS response message received => store in cache when not truncated and return
if (!$message->tc) {
$cache->set($id, $message, $that->ttl($message));
}
return $message;
}
);
}
)->then($resolve, function ($e) use ($reject, &$pending) {
$reject($e);
$pending = null;
});
}, function ($_, $reject) use (&$pending, $query) {
$reject(new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled'));
$pending->cancel();
$pending = null;
});
}
/**
* @param Message $message
* @return int
* @internal
*/
public function ttl(Message $message)
{
// select TTL from answers (should all be the same), use smallest value if available
// @link https://tools.ietf.org/html/rfc2181#section-5.2
$ttl = null;
foreach ($message->answers as $answer) {
if ($ttl === null || $answer->ttl < $ttl) {
$ttl = $answer->ttl;
}
}
if ($ttl === null) {
$ttl = self::TTL;
}
return $ttl;
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns\Query;
final class CancellationException extends \RuntimeException
{
}

View File

@@ -0,0 +1,91 @@
<?php
namespace React\Dns\Query;
use React\Promise\Promise;
/**
* Cooperatively resolves hosts via the given base executor to ensure same query is not run concurrently
*
* Wraps an existing `ExecutorInterface` to keep tracking of pending queries
* and only starts a new query when the same query is not already pending. Once
* the underlying query is fulfilled/rejected, it will forward its value to all
* promises awaiting the same query.
*
* This means it will not limit concurrency for queries that differ, for example
* when sending many queries for different host names or types.
*
* This is useful because all executors are entirely async and as such allow you
* to execute any number of queries concurrently. You should probably limit the
* number of concurrent queries in your application or you're very likely going
* to face rate limitations and bans on the resolver end. For many common
* applications, you may want to avoid sending the same query multiple times
* when the first one is still pending, so you will likely want to use this in
* combination with some other executor like this:
*
* ```php
* $executor = new CoopExecutor(
* new RetryExecutor(
* new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* )
* )
* );
* ```
*/
final class CoopExecutor implements ExecutorInterface
{
private $executor;
private $pending = array();
private $counts = array();
public function __construct(ExecutorInterface $base)
{
$this->executor = $base;
}
public function query(Query $query)
{
$key = $this->serializeQueryToIdentity($query);
if (isset($this->pending[$key])) {
// same query is already pending, so use shared reference to pending query
$promise = $this->pending[$key];
++$this->counts[$key];
} else {
// no such query pending, so start new query and keep reference until it's fulfilled or rejected
$promise = $this->executor->query($query);
$this->pending[$key] = $promise;
$this->counts[$key] = 1;
$pending =& $this->pending;
$counts =& $this->counts;
$promise->then(function () use ($key, &$pending, &$counts) {
unset($pending[$key], $counts[$key]);
}, function () use ($key, &$pending, &$counts) {
unset($pending[$key], $counts[$key]);
});
}
// Return a child promise awaiting the pending query.
// Cancelling this child promise should only cancel the pending query
// when no other child promise is awaiting the same query.
$pending =& $this->pending;
$counts =& $this->counts;
return new Promise(function ($resolve, $reject) use ($promise) {
$promise->then($resolve, $reject);
}, function () use (&$promise, $key, $query, &$pending, &$counts) {
if (--$counts[$key] < 1) {
unset($pending[$key], $counts[$key]);
$promise->cancel();
$promise = null;
}
throw new \RuntimeException('DNS query for ' . $query->describe() . ' has been cancelled');
});
}
private function serializeQueryToIdentity(Query $query)
{
return sprintf('%s:%s:%s', $query->name, $query->type, $query->class);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace React\Dns\Query;
interface ExecutorInterface
{
/**
* Executes a query and will return a response message
*
* It returns a Promise which either fulfills with a response
* `React\Dns\Model\Message` on success or rejects with an `Exception` if
* the query is not successful. A response message may indicate an error
* condition in its `rcode`, but this is considered a valid response message.
*
* ```php
* $executor->query($query)->then(
* function (React\Dns\Model\Message $response) {
* // response message successfully received
* var_dump($response->rcode, $response->answers);
* },
* function (Exception $error) {
* // failed to query due to $error
* }
* );
* ```
*
* The returned Promise MUST be implemented in such a way that it can be
* cancelled when it is still pending. Cancelling a pending promise MUST
* reject its value with an Exception. It SHOULD clean up any underlying
* resources and references as applicable.
*
* ```php
* $promise = $executor->query($query);
*
* $promise->cancel();
* ```
*
* @param Query $query
* @return \React\Promise\PromiseInterface<\React\Dns\Model\Message>
* resolves with response message on success or rejects with an Exception on error
*/
public function query(Query $query);
}

View File

@@ -0,0 +1,49 @@
<?php
namespace React\Dns\Query;
use React\Promise\Promise;
final class FallbackExecutor implements ExecutorInterface
{
private $executor;
private $fallback;
public function __construct(ExecutorInterface $executor, ExecutorInterface $fallback)
{
$this->executor = $executor;
$this->fallback = $fallback;
}
public function query(Query $query)
{
$cancelled = false;
$fallback = $this->fallback;
$promise = $this->executor->query($query);
return new Promise(function ($resolve, $reject) use (&$promise, $fallback, $query, &$cancelled) {
$promise->then($resolve, function (\Exception $e1) use ($fallback, $query, $resolve, $reject, &$cancelled, &$promise) {
// reject if primary resolution rejected due to cancellation
if ($cancelled) {
$reject($e1);
return;
}
// start fallback query if primary query rejected
$promise = $fallback->query($query)->then($resolve, function (\Exception $e2) use ($e1, $reject) {
$append = $e2->getMessage();
if (($pos = strpos($append, ':')) !== false) {
$append = substr($append, $pos + 2);
}
// reject with combined error message if both queries fail
$reject(new \RuntimeException($e1->getMessage() . '. ' . $append));
});
});
}, function () use (&$promise, &$cancelled) {
// cancel pending query (primary or fallback)
$cancelled = true;
$promise->cancel();
});
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace React\Dns\Query;
use React\Dns\Config\HostsFile;
use React\Dns\Model\Message;
use React\Dns\Model\Record;
use React\Promise;
/**
* Resolves hosts from the given HostsFile or falls back to another executor
*
* If the host is found in the hosts file, it will not be passed to the actual
* DNS executor. If the host is not found in the hosts file, it will be passed
* to the DNS executor as a fallback.
*/
final class HostsFileExecutor implements ExecutorInterface
{
private $hosts;
private $fallback;
public function __construct(HostsFile $hosts, ExecutorInterface $fallback)
{
$this->hosts = $hosts;
$this->fallback = $fallback;
}
public function query(Query $query)
{
if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
// forward lookup for type A or AAAA
$records = array();
$expectsColon = $query->type === Message::TYPE_AAAA;
foreach ($this->hosts->getIpsForHost($query->name) as $ip) {
// ensure this is an IPv4/IPV6 address according to query type
if ((strpos($ip, ':') !== false) === $expectsColon) {
$records[] = new Record($query->name, $query->type, $query->class, 0, $ip);
}
}
if ($records) {
return Promise\resolve(
Message::createResponseWithAnswersForQuery($query, $records)
);
}
} elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) {
// reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain
$ip = $this->getIpFromHost($query->name);
if ($ip !== null) {
$records = array();
foreach ($this->hosts->getHostsForIp($ip) as $host) {
$records[] = new Record($query->name, $query->type, $query->class, 0, $host);
}
if ($records) {
return Promise\resolve(
Message::createResponseWithAnswersForQuery($query, $records)
);
}
}
}
return $this->fallback->query($query);
}
private function getIpFromHost($host)
{
if (substr($host, -13) === '.in-addr.arpa') {
// IPv4: read as IP and reverse bytes
$ip = @inet_pton(substr($host, 0, -13));
if ($ip === false || isset($ip[4])) {
return null;
}
return inet_ntop(strrev($ip));
} elseif (substr($host, -9) === '.ip6.arpa') {
// IPv6: replace dots, reverse nibbles and interpret as hexadecimal string
$ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9)))));
if ($ip === false) {
return null;
}
return $ip;
} else {
return null;
}
}
}

69
vendor/react/dns/src/Query/Query.php vendored Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace React\Dns\Query;
use React\Dns\Model\Message;
/**
* This class represents a single question in a query/response message
*
* It uses a structure similar to `\React\Dns\Message\Record`, but does not
* contain fields for resulting TTL and resulting record data (IPs etc.).
*
* @link https://tools.ietf.org/html/rfc1035#section-4.1.2
* @see \React\Dns\Message\Record
*/
final class Query
{
/**
* @var string query name, i.e. hostname to look up
*/
public $name;
/**
* @var int query type (aka QTYPE), see Message::TYPE_* constants
*/
public $type;
/**
* @var int query class (aka QCLASS), see Message::CLASS_IN constant
*/
public $class;
/**
* @param string $name query name, i.e. hostname to look up
* @param int $type query type, see Message::TYPE_* constants
* @param int $class query class, see Message::CLASS_IN constant
*/
public function __construct($name, $type, $class)
{
$this->name = $name;
$this->type = $type;
$this->class = $class;
}
/**
* Describes the hostname and query type/class for this query
*
* The output format is supposed to be human readable and is subject to change.
* The format is inspired by RFC 3597 when handling unkown types/classes.
*
* @return string "example.com (A)" or "example.com (CLASS0 TYPE1234)"
* @link https://tools.ietf.org/html/rfc3597
*/
public function describe()
{
$class = $this->class !== Message::CLASS_IN ? 'CLASS' . $this->class . ' ' : '';
$type = 'TYPE' . $this->type;
$ref = new \ReflectionClass('React\Dns\Model\Message');
foreach ($ref->getConstants() as $name => $value) {
if ($value === $this->type && \strpos($name, 'TYPE_') === 0) {
$type = \substr($name, 5);
break;
}
}
return $this->name . ' (' . $class . $type . ')';
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace React\Dns\Query;
use React\Promise\Deferred;
use React\Promise\PromiseInterface;
final class RetryExecutor implements ExecutorInterface
{
private $executor;
private $retries;
public function __construct(ExecutorInterface $executor, $retries = 2)
{
$this->executor = $executor;
$this->retries = $retries;
}
public function query(Query $query)
{
return $this->tryQuery($query, $this->retries);
}
public function tryQuery(Query $query, $retries)
{
$deferred = new Deferred(function () use (&$promise) {
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
});
$success = function ($value) use ($deferred, &$errorback) {
$errorback = null;
$deferred->resolve($value);
};
$executor = $this->executor;
$errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries, $executor) {
if (!$e instanceof TimeoutException) {
$errorback = null;
$deferred->reject($e);
} elseif ($retries <= 0) {
$errorback = null;
$deferred->reject($e = new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: too many retries',
0,
$e
));
// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$r->setAccessible(true);
$trace = $r->getValue($e);
// Exception trace arguments are not available on some PHP 7.4 installs
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($e, $trace);
} else {
--$retries;
$promise = $executor->query($query)->then(
$success,
$errorback
);
}
};
$promise = $this->executor->query($query)->then(
$success,
$errorback
);
return $deferred->promise();
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace React\Dns\Query;
use React\Promise\Promise;
/**
* Send DNS queries over a UDP or TCP/IP stream transport.
*
* This class will automatically choose the correct transport protocol to send
* a DNS query to your DNS server. It will always try to send it over the more
* efficient UDP transport first. If this query yields a size related issue
* (truncated messages), it will retry over a streaming TCP/IP transport.
*
* For more advanced usages one can utilize this class directly.
* The following example looks up the `IPv6` address for `reactphp.org`.
*
* ```php
* $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor);
*
* $executor->query(
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
* )->then(function (Message $message) {
* foreach ($message->answers as $answer) {
* echo 'IPv6: ' . $answer->data . PHP_EOL;
* }
* }, 'printf');
* ```
*
* Note that this executor only implements the logic to select the correct
* transport for the given DNS query. Implementing the correct transport logic,
* implementing timeouts and any retry logic is left up to the given executors,
* see also [`UdpTransportExecutor`](#udptransportexecutor) and
* [`TcpTransportExecutor`](#tcptransportexecutor) for more details.
*
* Note that this executor is entirely async and as such allows you to execute
* any number of queries concurrently. You should probably limit the number of
* concurrent queries in your application or you're very likely going to face
* rate limitations and bans on the resolver end. For many common applications,
* you may want to avoid sending the same query multiple times when the first
* one is still pending, so you will likely want to use this in combination with
* a `CoopExecutor` like this:
*
* ```php
* $executor = new CoopExecutor(
* new SelectiveTransportExecutor(
* $datagramExecutor,
* $streamExecutor
* )
* );
* ```
*/
class SelectiveTransportExecutor implements ExecutorInterface
{
private $datagramExecutor;
private $streamExecutor;
public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor)
{
$this->datagramExecutor = $datagramExecutor;
$this->streamExecutor = $streamExecutor;
}
public function query(Query $query)
{
$stream = $this->streamExecutor;
$pending = $this->datagramExecutor->query($query);
return new Promise(function ($resolve, $reject) use (&$pending, $stream, $query) {
$pending->then(
$resolve,
function ($e) use (&$pending, $stream, $query, $resolve, $reject) {
if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) {
$pending = $stream->query($query)->then($resolve, $reject);
} else {
$reject($e);
}
}
);
}, function () use (&$pending) {
$pending->cancel();
$pending = null;
});
}
}

View File

@@ -0,0 +1,382 @@
<?php
namespace React\Dns\Query;
use React\Dns\Model\Message;
use React\Dns\Protocol\BinaryDumper;
use React\Dns\Protocol\Parser;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
/**
* Send DNS queries over a TCP/IP stream transport.
*
* This is one of the main classes that send a DNS query to your DNS server.
*
* For more advanced usages one can utilize this class directly.
* The following example looks up the `IPv6` address for `reactphp.org`.
*
* ```php
* $executor = new TcpTransportExecutor('8.8.8.8:53');
*
* $executor->query(
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
* )->then(function (Message $message) {
* foreach ($message->answers as $answer) {
* echo 'IPv6: ' . $answer->data . PHP_EOL;
* }
* }, 'printf');
* ```
*
* See also [example #92](examples).
*
* Note that this executor does not implement a timeout, so you will very likely
* want to use this in combination with a `TimeoutExecutor` like this:
*
* ```php
* $executor = new TimeoutExecutor(
* new TcpTransportExecutor($nameserver),
* 3.0
* );
* ```
*
* Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
* transport, so you do not necessarily have to implement any retry logic.
*
* Note that this executor is entirely async and as such allows you to execute
* queries concurrently. The first query will establish a TCP/IP socket
* connection to the DNS server which will be kept open for a short period.
* Additional queries will automatically reuse this existing socket connection
* to the DNS server, will pipeline multiple requests over this single
* connection and will keep an idle connection open for a short period. The
* initial TCP/IP connection overhead may incur a slight delay if you only send
* occasional queries when sending a larger number of concurrent queries over
* an existing connection, it becomes increasingly more efficient and avoids
* creating many concurrent sockets like the UDP-based executor. You may still
* want to limit the number of (concurrent) queries in your application or you
* may be facing rate limitations and bans on the resolver end. For many common
* applications, you may want to avoid sending the same query multiple times
* when the first one is still pending, so you will likely want to use this in
* combination with a `CoopExecutor` like this:
*
* ```php
* $executor = new CoopExecutor(
* new TimeoutExecutor(
* new TcpTransportExecutor($nameserver),
* 3.0
* )
* );
* ```
*
* > Internally, this class uses PHP's TCP/IP sockets and does not take advantage
* of [react/socket](https://github.com/reactphp/socket) purely for
* organizational reasons to avoid a cyclic dependency between the two
* packages. Higher-level components should take advantage of the Socket
* component instead of reimplementing this socket logic from scratch.
*/
class TcpTransportExecutor implements ExecutorInterface
{
private $nameserver;
private $loop;
private $parser;
private $dumper;
/**
* @var ?resource
*/
private $socket;
/**
* @var Deferred[]
*/
private $pending = array();
/**
* @var string[]
*/
private $names = array();
/**
* Maximum idle time when socket is current unused (i.e. no pending queries outstanding)
*
* If a new query is to be sent during the idle period, we can reuse the
* existing socket without having to wait for a new socket connection.
* This uses a rather small, hard-coded value to not keep any unneeded
* sockets open and to not keep the loop busy longer than needed.
*
* A future implementation may take advantage of `edns-tcp-keepalive` to keep
* the socket open for longer periods. This will likely require explicit
* configuration because this may consume additional resources and also keep
* the loop busy for longer than expected in some applications.
*
* @var float
* @link https://tools.ietf.org/html/rfc7766#section-6.2.1
* @link https://tools.ietf.org/html/rfc7828
*/
private $idlePeriod = 0.001;
/**
* @var ?\React\EventLoop\TimerInterface
*/
private $idleTimer;
private $writeBuffer = '';
private $writePending = false;
private $readBuffer = '';
private $readPending = false;
/** @var string */
private $readChunk = 0xffff;
/**
* @param string $nameserver
* @param ?LoopInterface $loop
*/
public function __construct($nameserver, $loop = null)
{
if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
// several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
$nameserver = '[' . $nameserver . ']';
}
$parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver);
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
throw new \InvalidArgumentException('Invalid nameserver address given');
}
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
$this->loop = $loop ?: Loop::get();
$this->parser = new Parser();
$this->dumper = new BinaryDumper();
}
public function query(Query $query)
{
$request = Message::createRequestForQuery($query);
// keep shuffing message ID to avoid using the same message ID for two pending queries at the same time
while (isset($this->pending[$request->id])) {
$request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore
}
$queryData = $this->dumper->toBinary($request);
$length = \strlen($queryData);
if ($length > 0xffff) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport'
));
}
$queryData = \pack('n', $length) . $queryData;
if ($this->socket === null) {
// create async TCP/IP connection (may take a while)
$socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT);
if ($socket === false) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
));
}
// set socket to non-blocking and wait for it to become writable (connection success/rejected)
\stream_set_blocking($socket, false);
if (\function_exists('stream_set_chunk_size')) {
\stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore
}
$this->socket = $socket;
}
if ($this->idleTimer !== null) {
$this->loop->cancelTimer($this->idleTimer);
$this->idleTimer = null;
}
// wait for socket to become writable to actually write out data
$this->writeBuffer .= $queryData;
if (!$this->writePending) {
$this->writePending = true;
$this->loop->addWriteStream($this->socket, array($this, 'handleWritable'));
}
$names =& $this->names;
$that = $this;
$deferred = new Deferred(function () use ($that, &$names, $request) {
// remove from list of pending names, but remember pending query
$name = $names[$request->id];
unset($names[$request->id]);
$that->checkIdle();
throw new CancellationException('DNS query for ' . $name . ' has been cancelled');
});
$this->pending[$request->id] = $deferred;
$this->names[$request->id] = $query->describe();
return $deferred->promise();
}
/**
* @internal
*/
public function handleWritable()
{
if ($this->readPending === false) {
$name = @\stream_socket_get_name($this->socket, true);
if ($name === false) {
// Connection failed? Check socket error if available for underlying errno/errstr.
// @codeCoverageIgnoreStart
if (\function_exists('socket_import_stream')) {
$socket = \socket_import_stream($this->socket);
$errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
$errstr = \socket_strerror($errno);
} else {
$errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
$errstr = 'Connection refused';
}
// @codeCoverageIgnoreEnd
$this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno);
return;
}
$this->readPending = true;
$this->loop->addReadStream($this->socket, array($this, 'handleRead'));
}
$errno = 0;
$errstr = '';
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
// Match errstr from PHP's warning message.
// fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe
\preg_match('/errno=(\d+) (.+)/', $error, $m);
$errno = isset($m[1]) ? (int) $m[1] : 0;
$errstr = isset($m[2]) ? $m[2] : $error;
});
$written = \fwrite($this->socket, $this->writeBuffer);
\restore_error_handler();
if ($written === false || $written === 0) {
$this->closeError(
'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
);
return;
}
if (isset($this->writeBuffer[$written])) {
$this->writeBuffer = \substr($this->writeBuffer, $written);
} else {
$this->loop->removeWriteStream($this->socket);
$this->writePending = false;
$this->writeBuffer = '';
}
}
/**
* @internal
*/
public function handleRead()
{
// read one chunk of data from the DNS server
// any error is fatal, this is a stream of TCP/IP data
$chunk = @\fread($this->socket, $this->readChunk);
if ($chunk === false || $chunk === '') {
$this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
return;
}
// reassemble complete message by concatenating all chunks.
$this->readBuffer .= $chunk;
// response message header contains at least 12 bytes
while (isset($this->readBuffer[11])) {
// read response message length from first 2 bytes and ensure we have length + data in buffer
list(, $length) = \unpack('n', $this->readBuffer);
if (!isset($this->readBuffer[$length + 1])) {
return;
}
$data = \substr($this->readBuffer, 2, $length);
$this->readBuffer = (string)substr($this->readBuffer, $length + 2);
try {
$response = $this->parser->parseMessage($data);
} catch (\Exception $e) {
// reject all pending queries if we received an invalid message from remote server
$this->closeError('Invalid message received from DNS server ' . $this->nameserver);
return;
}
// reject all pending queries if we received an unexpected response ID or truncated response
if (!isset($this->pending[$response->id]) || $response->tc) {
$this->closeError('Invalid response message received from DNS server ' . $this->nameserver);
return;
}
$deferred = $this->pending[$response->id];
unset($this->pending[$response->id], $this->names[$response->id]);
$deferred->resolve($response);
$this->checkIdle();
}
}
/**
* @internal
* @param string $reason
* @param int $code
*/
public function closeError($reason, $code = 0)
{
$this->readBuffer = '';
if ($this->readPending) {
$this->loop->removeReadStream($this->socket);
$this->readPending = false;
}
$this->writeBuffer = '';
if ($this->writePending) {
$this->loop->removeWriteStream($this->socket);
$this->writePending = false;
}
if ($this->idleTimer !== null) {
$this->loop->cancelTimer($this->idleTimer);
$this->idleTimer = null;
}
@\fclose($this->socket);
$this->socket = null;
foreach ($this->names as $id => $name) {
$this->pending[$id]->reject(new \RuntimeException(
'DNS query for ' . $name . ' failed: ' . $reason,
$code
));
}
$this->pending = $this->names = array();
}
/**
* @internal
*/
public function checkIdle()
{
if ($this->idleTimer === null && !$this->names) {
$that = $this;
$this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) {
$that->closeError('Idle timeout');
});
}
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns\Query;
final class TimeoutException extends \Exception
{
}

View File

@@ -0,0 +1,78 @@
<?php
namespace React\Dns\Query;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Promise;
final class TimeoutExecutor implements ExecutorInterface
{
private $executor;
private $loop;
private $timeout;
/**
* @param ExecutorInterface $executor
* @param float $timeout
* @param ?LoopInterface $loop
*/
public function __construct(ExecutorInterface $executor, $timeout, $loop = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->executor = $executor;
$this->loop = $loop ?: Loop::get();
$this->timeout = $timeout;
}
public function query(Query $query)
{
$promise = $this->executor->query($query);
$loop = $this->loop;
$time = $this->timeout;
return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $query) {
$timer = null;
$promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
if ($timer) {
$loop->cancelTimer($timer);
}
$timer = false;
$resolve($v);
}, function ($v) use (&$timer, $loop, $reject) {
if ($timer) {
$loop->cancelTimer($timer);
}
$timer = false;
$reject($v);
});
// promise already resolved => no need to start timer
if ($timer === false) {
return;
}
// start timeout timer which will cancel the pending promise
$timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $query) {
$reject(new TimeoutException(
'DNS query for ' . $query->describe() . ' timed out'
));
// Cancel pending query to clean up any underlying resources and references.
// Avoid garbage references in call stack by passing pending promise by reference.
assert(\method_exists($promise, 'cancel'));
$promise->cancel();
$promise = null;
});
}, function () use (&$promise) {
// Cancelling this promise will cancel the pending query, thus triggering the rejection logic above.
// Avoid garbage references in call stack by passing pending promise by reference.
assert(\method_exists($promise, 'cancel'));
$promise->cancel();
$promise = null;
});
}
}

View File

@@ -0,0 +1,221 @@
<?php
namespace React\Dns\Query;
use React\Dns\Model\Message;
use React\Dns\Protocol\BinaryDumper;
use React\Dns\Protocol\Parser;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
/**
* Send DNS queries over a UDP transport.
*
* This is the main class that sends a DNS query to your DNS server and is used
* internally by the `Resolver` for the actual message transport.
*
* For more advanced usages one can utilize this class directly.
* The following example looks up the `IPv6` address for `igor.io`.
*
* ```php
* $executor = new UdpTransportExecutor('8.8.8.8:53');
*
* $executor->query(
* new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
* )->then(function (Message $message) {
* foreach ($message->answers as $answer) {
* echo 'IPv6: ' . $answer->data . PHP_EOL;
* }
* }, 'printf');
* ```
*
* See also the [fourth example](examples).
*
* Note that this executor does not implement a timeout, so you will very likely
* want to use this in combination with a `TimeoutExecutor` like this:
*
* ```php
* $executor = new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* );
* ```
*
* Also note that this executor uses an unreliable UDP transport and that it
* does not implement any retry logic, so you will likely want to use this in
* combination with a `RetryExecutor` like this:
*
* ```php
* $executor = new RetryExecutor(
* new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* )
* );
* ```
*
* Note that this executor is entirely async and as such allows you to execute
* any number of queries concurrently. You should probably limit the number of
* concurrent queries in your application or you're very likely going to face
* rate limitations and bans on the resolver end. For many common applications,
* you may want to avoid sending the same query multiple times when the first
* one is still pending, so you will likely want to use this in combination with
* a `CoopExecutor` like this:
*
* ```php
* $executor = new CoopExecutor(
* new RetryExecutor(
* new TimeoutExecutor(
* new UdpTransportExecutor($nameserver),
* 3.0
* )
* )
* );
* ```
*
* > Internally, this class uses PHP's UDP sockets and does not take advantage
* of [react/datagram](https://github.com/reactphp/datagram) purely for
* organizational reasons to avoid a cyclic dependency between the two
* packages. Higher-level components should take advantage of the Datagram
* component instead of reimplementing this socket logic from scratch.
*/
final class UdpTransportExecutor implements ExecutorInterface
{
private $nameserver;
private $loop;
private $parser;
private $dumper;
/**
* maximum UDP packet size to send and receive
*
* @var int
*/
private $maxPacketSize = 512;
/**
* @param string $nameserver
* @param ?LoopInterface $loop
*/
public function __construct($nameserver, $loop = null)
{
if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
// several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
$nameserver = '[' . $nameserver . ']';
}
$parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver);
if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
throw new \InvalidArgumentException('Invalid nameserver address given');
}
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
$this->loop = $loop ?: Loop::get();
$this->parser = new Parser();
$this->dumper = new BinaryDumper();
}
public function query(Query $query)
{
$request = Message::createRequestForQuery($query);
$queryData = $this->dumper->toBinary($request);
if (isset($queryData[$this->maxPacketSize])) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Query too large for UDP transport',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
}
// UDP connections are instant, so try connection without a loop or timeout
$errno = 0;
$errstr = '';
$socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0);
if ($socket === false) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
));
}
// set socket to non-blocking and immediately try to send (fill write buffer)
\stream_set_blocking($socket, false);
\set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
// Write may potentially fail, but most common errors are already caught by connection check above.
// Among others, macOS is known to report here when trying to send to broadcast address.
// This can also be reproduced by writing data exceeding `stream_set_chunk_size()` to a server refusing UDP data.
// fwrite(): send of 8192 bytes failed with errno=111 Connection refused
\preg_match('/errno=(\d+) (.+)/', $error, $m);
$errno = isset($m[1]) ? (int) $m[1] : 0;
$errstr = isset($m[2]) ? $m[2] : $error;
});
$written = \fwrite($socket, $queryData);
\restore_error_handler();
if ($written !== \strlen($queryData)) {
return \React\Promise\reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
$errno
));
}
$loop = $this->loop;
$deferred = new Deferred(function () use ($loop, $socket, $query) {
// cancellation should remove socket from loop and close socket
$loop->removeReadStream($socket);
\fclose($socket);
throw new CancellationException('DNS query for ' . $query->describe() . ' has been cancelled');
});
$max = $this->maxPacketSize;
$parser = $this->parser;
$nameserver = $this->nameserver;
$loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request, $max, $nameserver) {
// try to read a single data packet from the DNS server
// ignoring any errors, this is uses UDP packets and not a stream of data
$data = @\fread($socket, $max);
if ($data === false) {
return;
}
try {
$response = $parser->parseMessage($data);
} catch (\Exception $e) {
// ignore and await next if we received an invalid message from remote server
// this may as well be a fake response from an attacker (possible DOS)
return;
}
// ignore and await next if we received an unexpected response ID
// this may as well be a fake response from an attacker (possible cache poisoning)
if ($response->id !== $request->id) {
return;
}
// we only react to the first valid message, so remove socket from loop and close
$loop->removeReadStream($socket);
\fclose($socket);
if ($response->tc) {
$deferred->reject(new \RuntimeException(
'DNS query for ' . $query->describe() . ' failed: The DNS server ' . $nameserver . ' returned a truncated result for a UDP query',
\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
));
return;
}
$deferred->resolve($response);
});
return $deferred->promise();
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace React\Dns;
final class RecordNotFoundException extends \Exception
{
}

View File

@@ -0,0 +1,226 @@
<?php
namespace React\Dns\Resolver;
use React\Cache\ArrayCache;
use React\Cache\CacheInterface;
use React\Dns\Config\Config;
use React\Dns\Config\HostsFile;
use React\Dns\Query\CachingExecutor;
use React\Dns\Query\CoopExecutor;
use React\Dns\Query\ExecutorInterface;
use React\Dns\Query\FallbackExecutor;
use React\Dns\Query\HostsFileExecutor;
use React\Dns\Query\RetryExecutor;
use React\Dns\Query\SelectiveTransportExecutor;
use React\Dns\Query\TcpTransportExecutor;
use React\Dns\Query\TimeoutExecutor;
use React\Dns\Query\UdpTransportExecutor;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
final class Factory
{
/**
* Creates a DNS resolver instance for the given DNS config
*
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
* single nameserver address. If the given config contains more than one DNS
* nameserver, all DNS nameservers will be used in order. The primary DNS
* server will always be used first before falling back to the secondary or
* tertiary DNS server.
*
* @param Config|string $config DNS Config object (recommended) or single nameserver address
* @param ?LoopInterface $loop
* @return \React\Dns\Resolver\ResolverInterface
* @throws \InvalidArgumentException for invalid DNS server address
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
*/
public function create($config, $loop = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
$executor = $this->decorateHostsFileExecutor($this->createExecutor($config, $loop ?: Loop::get()));
return new Resolver($executor);
}
/**
* Creates a cached DNS resolver instance for the given DNS config and cache
*
* As of v1.7.0 it's recommended to pass a `Config` object instead of a
* single nameserver address. If the given config contains more than one DNS
* nameserver, all DNS nameservers will be used in order. The primary DNS
* server will always be used first before falling back to the secondary or
* tertiary DNS server.
*
* @param Config|string $config DNS Config object (recommended) or single nameserver address
* @param ?LoopInterface $loop
* @param ?CacheInterface $cache
* @return \React\Dns\Resolver\ResolverInterface
* @throws \InvalidArgumentException for invalid DNS server address
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
*/
public function createCached($config, $loop = null, $cache = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
}
if ($cache !== null && !$cache instanceof CacheInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #3 ($cache) expected null|React\Cache\CacheInterface');
}
// default to keeping maximum of 256 responses in cache unless explicitly given
if (!($cache instanceof CacheInterface)) {
$cache = new ArrayCache(256);
}
$executor = $this->createExecutor($config, $loop ?: Loop::get());
$executor = new CachingExecutor($executor, $cache);
$executor = $this->decorateHostsFileExecutor($executor);
return new Resolver($executor);
}
/**
* Tries to load the hosts file and decorates the given executor on success
*
* @param ExecutorInterface $executor
* @return ExecutorInterface
* @codeCoverageIgnore
*/
private function decorateHostsFileExecutor(ExecutorInterface $executor)
{
try {
$executor = new HostsFileExecutor(
HostsFile::loadFromPathBlocking(),
$executor
);
} catch (\RuntimeException $e) {
// ignore this file if it can not be loaded
}
// Windows does not store localhost in hosts file by default but handles this internally
// To compensate for this, we explicitly use hard-coded defaults for localhost
if (DIRECTORY_SEPARATOR === '\\') {
$executor = new HostsFileExecutor(
new HostsFile("127.0.0.1 localhost\n::1 localhost"),
$executor
);
}
return $executor;
}
/**
* @param Config|string $nameserver
* @param LoopInterface $loop
* @return CoopExecutor
* @throws \InvalidArgumentException for invalid DNS server address
* @throws \UnderflowException when given DNS Config object has an empty list of nameservers
*/
private function createExecutor($nameserver, LoopInterface $loop)
{
if ($nameserver instanceof Config) {
if (!$nameserver->nameservers) {
throw new \UnderflowException('Empty config with no DNS servers');
}
// Hard-coded to check up to 3 DNS servers to match default limits in place in most systems (see MAXNS config).
// Note to future self: Recursion isn't too hard, but how deep do we really want to go?
$primary = reset($nameserver->nameservers);
$secondary = next($nameserver->nameservers);
$tertiary = next($nameserver->nameservers);
if ($tertiary !== false) {
// 3 DNS servers given => nest first with fallback for second and third
return new CoopExecutor(
new RetryExecutor(
new FallbackExecutor(
$this->createSingleExecutor($primary, $loop),
new FallbackExecutor(
$this->createSingleExecutor($secondary, $loop),
$this->createSingleExecutor($tertiary, $loop)
)
)
)
);
} elseif ($secondary !== false) {
// 2 DNS servers given => fallback from first to second
return new CoopExecutor(
new RetryExecutor(
new FallbackExecutor(
$this->createSingleExecutor($primary, $loop),
$this->createSingleExecutor($secondary, $loop)
)
)
);
} else {
// 1 DNS server given => use single executor
$nameserver = $primary;
}
}
return new CoopExecutor(new RetryExecutor($this->createSingleExecutor($nameserver, $loop)));
}
/**
* @param string $nameserver
* @param LoopInterface $loop
* @return ExecutorInterface
* @throws \InvalidArgumentException for invalid DNS server address
*/
private function createSingleExecutor($nameserver, LoopInterface $loop)
{
$parts = \parse_url($nameserver);
if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') {
$executor = $this->createTcpExecutor($nameserver, $loop);
} elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') {
$executor = $this->createUdpExecutor($nameserver, $loop);
} else {
$executor = new SelectiveTransportExecutor(
$this->createUdpExecutor($nameserver, $loop),
$this->createTcpExecutor($nameserver, $loop)
);
}
return $executor;
}
/**
* @param string $nameserver
* @param LoopInterface $loop
* @return TimeoutExecutor
* @throws \InvalidArgumentException for invalid DNS server address
*/
private function createTcpExecutor($nameserver, LoopInterface $loop)
{
return new TimeoutExecutor(
new TcpTransportExecutor($nameserver, $loop),
5.0,
$loop
);
}
/**
* @param string $nameserver
* @param LoopInterface $loop
* @return TimeoutExecutor
* @throws \InvalidArgumentException for invalid DNS server address
*/
private function createUdpExecutor($nameserver, LoopInterface $loop)
{
return new TimeoutExecutor(
new UdpTransportExecutor(
$nameserver,
$loop
),
5.0,
$loop
);
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace React\Dns\Resolver;
use React\Dns\Model\Message;
use React\Dns\Query\ExecutorInterface;
use React\Dns\Query\Query;
use React\Dns\RecordNotFoundException;
/**
* @see ResolverInterface for the base interface
*/
final class Resolver implements ResolverInterface
{
private $executor;
public function __construct(ExecutorInterface $executor)
{
$this->executor = $executor;
}
public function resolve($domain)
{
return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
return $ips[array_rand($ips)];
});
}
public function resolveAll($domain, $type)
{
$query = new Query($domain, $type, Message::CLASS_IN);
$that = $this;
return $this->executor->query(
$query
)->then(function (Message $response) use ($query, $that) {
return $that->extractValues($query, $response);
});
}
/**
* [Internal] extract all resource record values from response for this query
*
* @param Query $query
* @param Message $response
* @return array
* @throws RecordNotFoundException when response indicates an error or contains no data
* @internal
*/
public function extractValues(Query $query, Message $response)
{
// reject if response code indicates this is an error response message
$code = $response->rcode;
if ($code !== Message::RCODE_OK) {
switch ($code) {
case Message::RCODE_FORMAT_ERROR:
$message = 'Format Error';
break;
case Message::RCODE_SERVER_FAILURE:
$message = 'Server Failure';
break;
case Message::RCODE_NAME_ERROR:
$message = 'Non-Existent Domain / NXDOMAIN';
break;
case Message::RCODE_NOT_IMPLEMENTED:
$message = 'Not Implemented';
break;
case Message::RCODE_REFUSED:
$message = 'Refused';
break;
default:
$message = 'Unknown error response code ' . $code;
}
throw new RecordNotFoundException(
'DNS query for ' . $query->describe() . ' returned an error response (' . $message . ')',
$code
);
}
$answers = $response->answers;
$addresses = $this->valuesByNameAndType($answers, $query->name, $query->type);
// reject if we did not receive a valid answer (domain is valid, but no record for this type could be found)
if (0 === count($addresses)) {
throw new RecordNotFoundException(
'DNS query for ' . $query->describe() . ' did not return a valid answer (NOERROR / NODATA)'
);
}
return array_values($addresses);
}
/**
* @param \React\Dns\Model\Record[] $answers
* @param string $name
* @param int $type
* @return array
*/
private function valuesByNameAndType(array $answers, $name, $type)
{
// return all record values for this name and type (if any)
$named = $this->filterByName($answers, $name);
$records = $this->filterByType($named, $type);
if ($records) {
return $this->mapRecordData($records);
}
// no matching records found? check if there are any matching CNAMEs instead
$cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
if ($cnameRecords) {
$cnames = $this->mapRecordData($cnameRecords);
foreach ($cnames as $cname) {
$records = array_merge(
$records,
$this->valuesByNameAndType($answers, $cname, $type)
);
}
}
return $records;
}
private function filterByName(array $answers, $name)
{
return $this->filterByField($answers, 'name', $name);
}
private function filterByType(array $answers, $type)
{
return $this->filterByField($answers, 'type', $type);
}
private function filterByField(array $answers, $field, $value)
{
$value = strtolower($value);
return array_filter($answers, function ($answer) use ($field, $value) {
return $value === strtolower($answer->$field);
});
}
private function mapRecordData(array $records)
{
return array_map(function ($record) {
return $record->data;
}, $records);
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace React\Dns\Resolver;
interface ResolverInterface
{
/**
* Resolves the given $domain name to a single IPv4 address (type `A` query).
*
* ```php
* $resolver->resolve('reactphp.org')->then(function ($ip) {
* echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
* });
* ```
*
* This is one of the main methods in this package. It sends a DNS query
* for the given $domain name to your DNS server and returns a single IP
* address on success.
*
* If the DNS server sends a DNS response message that contains more than
* one IP address for this query, it will randomly pick one of the IP
* addresses from the response. If you want the full list of IP addresses
* or want to send a different type of query, you should use the
* [`resolveAll()`](#resolveall) method instead.
*
* If the DNS server sends a DNS response message that indicates an error
* code, this method will reject with a `RecordNotFoundException`. Its
* message and code can be used to check for the response code.
*
* If the DNS communication fails and the server does not respond with a
* valid response message, this message will reject with an `Exception`.
*
* Pending DNS queries can be cancelled by cancelling its pending promise like so:
*
* ```php
* $promise = $resolver->resolve('reactphp.org');
*
* $promise->cancel();
* ```
*
* @param string $domain
* @return \React\Promise\PromiseInterface<string>
* resolves with a single IP address on success or rejects with an Exception on error.
*/
public function resolve($domain);
/**
* Resolves all record values for the given $domain name and query $type.
*
* ```php
* $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
* echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
* });
*
* $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
* echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
* });
* ```
*
* This is one of the main methods in this package. It sends a DNS query
* for the given $domain name to your DNS server and returns a list with all
* record values on success.
*
* If the DNS server sends a DNS response message that contains one or more
* records for this query, it will return a list with all record values
* from the response. You can use the `Message::TYPE_*` constants to control
* which type of query will be sent. Note that this method always returns a
* list of record values, but each record value type depends on the query
* type. For example, it returns the IPv4 addresses for type `A` queries,
* the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
* `CNAME` and `PTR` queries and structured data for other queries. See also
* the `Record` documentation for more details.
*
* If the DNS server sends a DNS response message that indicates an error
* code, this method will reject with a `RecordNotFoundException`. Its
* message and code can be used to check for the response code.
*
* If the DNS communication fails and the server does not respond with a
* valid response message, this message will reject with an `Exception`.
*
* Pending DNS queries can be cancelled by cancelling its pending promise like so:
*
* ```php
* $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
*
* $promise->cancel();
* ```
*
* @param string $domain
* @return \React\Promise\PromiseInterface<array>
* Resolves with all record values on success or rejects with an Exception on error.
*/
public function resolveAll($domain, $type);
}