Subversion Repositories php-qbpwcf

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
3 liveuser 1
<?php
2
 
3
namespace React\Dns\Query;
4
 
5
use React\Dns\Model\Message;
6
use React\Dns\Protocol\BinaryDumper;
7
use React\Dns\Protocol\Parser;
8
use React\EventLoop\LoopInterface;
9
use React\Promise\Deferred;
10
 
11
/**
12
 * Send DNS queries over a UDP transport.
13
 *
14
 * This is the main class that sends a DNS query to your DNS server and is used
15
 * internally by the `Resolver` for the actual message transport.
16
 *
17
 * For more advanced usages one can utilize this class directly.
18
 * The following example looks up the `IPv6` address for `igor.io`.
19
 *
20
 * ```php
21
 * $loop = Factory::create();
22
 * $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
23
 *
24
 * $executor->query(
25
 *     new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
26
 * )->then(function (Message $message) {
27
 *     foreach ($message->answers as $answer) {
28
 *         echo 'IPv6: ' . $answer->data . PHP_EOL;
29
 *     }
30
 * }, 'printf');
31
 *
32
 * $loop->run();
33
 * ```
34
 *
35
 * See also the [fourth example](examples).
36
 *
37
 * Note that this executor does not implement a timeout, so you will very likely
38
 * want to use this in combination with a `TimeoutExecutor` like this:
39
 *
40
 * ```php
41
 * $executor = new TimeoutExecutor(
42
 *     new UdpTransportExecutor($nameserver, $loop),
43
 *     3.0,
44
 *     $loop
45
 * );
46
 * ```
47
 *
48
 * Also note that this executor uses an unreliable UDP transport and that it
49
 * does not implement any retry logic, so you will likely want to use this in
50
 * combination with a `RetryExecutor` like this:
51
 *
52
 * ```php
53
 * $executor = new RetryExecutor(
54
 *     new TimeoutExecutor(
55
 *         new UdpTransportExecutor($nameserver, $loop),
56
 *         3.0,
57
 *         $loop
58
 *     )
59
 * );
60
 * ```
61
 *
62
 * Note that this executor is entirely async and as such allows you to execute
63
 * any number of queries concurrently. You should probably limit the number of
64
 * concurrent queries in your application or you're very likely going to face
65
 * rate limitations and bans on the resolver end. For many common applications,
66
 * you may want to avoid sending the same query multiple times when the first
67
 * one is still pending, so you will likely want to use this in combination with
68
 * a `CoopExecutor` like this:
69
 *
70
 * ```php
71
 * $executor = new CoopExecutor(
72
 *     new RetryExecutor(
73
 *         new TimeoutExecutor(
74
 *             new UdpTransportExecutor($nameserver, $loop),
75
 *             3.0,
76
 *             $loop
77
 *         )
78
 *     )
79
 * );
80
 * ```
81
 *
82
 * > Internally, this class uses PHP's UDP sockets and does not take advantage
83
 *   of [react/datagram](https://github.com/reactphp/datagram) purely for
84
 *   organizational reasons to avoid a cyclic dependency between the two
85
 *   packages. Higher-level components should take advantage of the Datagram
86
 *   component instead of reimplementing this socket logic from scratch.
87
 */
88
final class UdpTransportExecutor implements ExecutorInterface
89
{
90
    private $nameserver;
91
    private $loop;
92
    private $parser;
93
    private $dumper;
94
 
95
    /**
96
     * @param string        $nameserver
97
     * @param LoopInterface $loop
98
     */
99
    public function __construct($nameserver, LoopInterface $loop)
100
    {
101
        if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
102
            // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
103
            $nameserver = '[' . $nameserver . ']';
104
        }
105
 
106
        $parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver);
107
        if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || !\filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) {
108
            throw new \InvalidArgumentException('Invalid nameserver address given');
109
        }
110
 
111
        $this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
112
        $this->loop = $loop;
113
        $this->parser = new Parser();
114
        $this->dumper = new BinaryDumper();
115
    }
116
 
117
    public function query(Query $query)
118
    {
119
        $request = Message::createRequestForQuery($query);
120
 
121
        $queryData = $this->dumper->toBinary($request);
122
        if (isset($queryData[512])) {
123
            return \React\Promise\reject(new \RuntimeException(
124
                'DNS query for ' . $query->name . ' failed: Query too large for UDP transport',
125
                \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
126
            ));
127
        }
128
 
129
        // UDP connections are instant, so try connection without a loop or timeout
130
        $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0);
131
        if ($socket === false) {
132
            return \React\Promise\reject(new \RuntimeException(
133
                'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server ('  . $errstr . ')',
134
                $errno
135
            ));
136
        }
137
 
138
        // set socket to non-blocking and immediately try to send (fill write buffer)
139
        \stream_set_blocking($socket, false);
140
        \fwrite($socket, $queryData);
141
 
142
        $loop = $this->loop;
143
        $deferred = new Deferred(function () use ($loop, $socket, $query) {
144
            // cancellation should remove socket from loop and close socket
145
            $loop->removeReadStream($socket);
146
            \fclose($socket);
147
 
148
            throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled');
149
        });
150
 
151
        $parser = $this->parser;
152
        $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request) {
153
            // try to read a single data packet from the DNS server
154
            // ignoring any errors, this is uses UDP packets and not a stream of data
155
            $data = @\fread($socket, 512);
156
 
157
            try {
158
                $response = $parser->parseMessage($data);
159
            } catch (\Exception $e) {
160
                // ignore and await next if we received an invalid message from remote server
161
                // this may as well be a fake response from an attacker (possible DOS)
162
                return;
163
            }
164
 
165
            // ignore and await next if we received an unexpected response ID
166
            // this may as well be a fake response from an attacker (possible cache poisoning)
167
            if ($response->id !== $request->id) {
168
                return;
169
            }
170
 
171
            // we only react to the first valid message, so remove socket from loop and close
172
            $loop->removeReadStream($socket);
173
            \fclose($socket);
174
 
175
            if ($response->tc) {
176
                $deferred->reject(new \RuntimeException(
177
                    'DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query',
178
                    \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90
179
                ));
180
                return;
181
            }
182
 
183
            $deferred->resolve($response);
184
        });
185
 
186
        return $deferred->promise();
187
    }
188
}