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\Protocol;
4
 
5
use React\Dns\Model\Message;
6
use React\Dns\Model\Record;
7
use React\Dns\Query\Query;
8
use InvalidArgumentException;
9
 
10
/**
11
 * DNS protocol parser
12
 *
13
 * Obsolete and uncommon types and classes are not implemented.
14
 */
15
final class Parser
16
{
17
    /**
18
     * Parses the given raw binary message into a Message object
19
     *
20
     * @param string $data
21
     * @throws InvalidArgumentException
22
     * @return Message
23
     */
24
    public function parseMessage($data)
25
    {
26
        // create empty message with two additional, temporary properties for parser
27
        $message = new Message();
28
        $message->data = $data;
29
        $message->consumed = null;
30
 
31
        if ($this->parse($data, $message) !== $message) {
32
            throw new InvalidArgumentException('Unable to parse binary message');
33
        }
34
 
35
        unset($message->data, $message->consumed);
36
 
37
        return $message;
38
    }
39
 
40
    private function parse($data, Message $message)
41
    {
42
        if (!isset($message->data[12 - 1])) {
43
            return;
44
        }
45
 
46
        list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($message->data, 0, 12)));
47
        $message->consumed += 12;
48
 
49
        $message->id = $id;
50
        $message->rcode = $fields & 0xf;
51
        $message->ra = (($fields >> 7) & 1) === 1;
52
        $message->rd = (($fields >> 8) & 1) === 1;
53
        $message->tc = (($fields >> 9) & 1) === 1;
54
        $message->aa = (($fields >> 10) & 1) === 1;
55
        $message->opcode = ($fields >> 11) & 0xf;
56
        $message->qr = (($fields >> 15) & 1) === 1;
57
 
58
        // parse all questions
59
        for ($i = $qdCount; $i > 0; --$i) {
60
            $question = $this->parseQuestion($message);
61
            if ($question === null) {
62
                return;
63
            } else {
64
                $message->questions[] = $question;
65
            }
66
        }
67
 
68
        // parse all answer records
69
        for ($i = $anCount; $i > 0; --$i) {
70
            $record = $this->parseRecord($message);
71
            if ($record === null) {
72
                return;
73
            } else {
74
                $message->answers[] = $record;
75
            }
76
        }
77
 
78
        // parse all authority records
79
        for ($i = $nsCount; $i > 0; --$i) {
80
            $record = $this->parseRecord($message);
81
            if ($record === null) {
82
                return;
83
            } else {
84
                $message->authority[] = $record;
85
            }
86
        }
87
 
88
        // parse all additional records
89
        for ($i = $arCount; $i > 0; --$i) {
90
            $record = $this->parseRecord($message);
91
            if ($record === null) {
92
                return;
93
            } else {
94
                $message->additional[] = $record;
95
            }
96
        }
97
 
98
        return $message;
99
    }
100
 
101
    /**
102
     * @param Message $message
103
     * @return ?Query
104
     */
105
    private function parseQuestion(Message $message)
106
    {
107
        $consumed = $message->consumed;
108
 
109
        list($labels, $consumed) = $this->readLabels($message->data, $consumed);
110
 
111
        if ($labels === null || !isset($message->data[$consumed + 4 - 1])) {
112
            return;
113
        }
114
 
115
        list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
116
        $consumed += 4;
117
 
118
        $message->consumed = $consumed;
119
 
120
        return new Query(
121
            implode('.', $labels),
122
            $type,
123
            $class
124
        );
125
    }
126
 
127
    /**
128
     * @param Message $message
129
     * @return ?Record returns parsed Record on success or null if data is invalid/incomplete
130
     */
131
    private function parseRecord(Message $message)
132
    {
133
        $consumed = $message->consumed;
134
 
135
        list($name, $consumed) = $this->readDomain($message->data, $consumed);
136
 
137
        if ($name === null || !isset($message->data[$consumed + 10 - 1])) {
138
            return null;
139
        }
140
 
141
        list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
142
        $consumed += 4;
143
 
144
        list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4)));
145
        $consumed += 4;
146
 
147
        // TTL is a UINT32 that must not have most significant bit set for BC reasons
148
        if ($ttl < 0 || $ttl >= 1 << 31) {
149
            $ttl = 0;
150
        }
151
 
152
        list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2)));
153
        $consumed += 2;
154
 
155
        if (!isset($message->data[$consumed + $rdLength - 1])) {
156
            return null;
157
        }
158
 
159
        $rdata = null;
160
        $expected = $consumed + $rdLength;
161
 
162
        if (Message::TYPE_A === $type) {
163
            if ($rdLength === 4) {
164
                $rdata = inet_ntop(substr($message->data, $consumed, $rdLength));
165
                $consumed += $rdLength;
166
            }
167
        } elseif (Message::TYPE_AAAA === $type) {
168
            if ($rdLength === 16) {
169
                $rdata = inet_ntop(substr($message->data, $consumed, $rdLength));
170
                $consumed += $rdLength;
171
            }
172
        } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
173
            list($rdata, $consumed) = $this->readDomain($message->data, $consumed);
174
        } elseif (Message::TYPE_TXT === $type) {
175
            $rdata = array();
176
            while ($consumed < $expected) {
177
                $len = ord($message->data[$consumed]);
178
                $rdata[] = (string)substr($message->data, $consumed + 1, $len);
179
                $consumed += $len + 1;
180
            }
181
        } elseif (Message::TYPE_MX === $type) {
182
            if ($rdLength > 2) {
183
                list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2)));
184
                list($target, $consumed) = $this->readDomain($message->data, $consumed + 2);
185
 
186
                $rdata = array(
187
                    'priority' => $priority,
188
                    'target' => $target
189
                );
190
            }
191
        } elseif (Message::TYPE_SRV === $type) {
192
            if ($rdLength > 6) {
193
                list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6)));
194
                list($target, $consumed) = $this->readDomain($message->data, $consumed + 6);
195
 
196
                $rdata = array(
197
                    'priority' => $priority,
198
                    'weight' => $weight,
199
                    'port' => $port,
200
                    'target' => $target
201
                );
202
            }
203
        } elseif (Message::TYPE_SSHFP === $type) {
204
            if ($rdLength > 2) {
205
                list($algorithm, $hash) = \array_values(\unpack('C*', \substr($message->data, $consumed, 2)));
206
                $fingerprint = \bin2hex(\substr($message->data, $consumed + 2, $rdLength - 2));
207
                $consumed += $rdLength;
208
 
209
                $rdata = array(
210
                    'algorithm' => $algorithm,
211
                    'type' => $hash,
212
                    'fingerprint' => $fingerprint
213
                );
214
            }
215
        } elseif (Message::TYPE_SOA === $type) {
216
            list($mname, $consumed) = $this->readDomain($message->data, $consumed);
217
            list($rname, $consumed) = $this->readDomain($message->data, $consumed);
218
 
219
            if ($mname !== null && $rname !== null && isset($message->data[$consumed + 20 - 1])) {
220
                list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20)));
221
                $consumed += 20;
222
 
223
                $rdata = array(
224
                    'mname' => $mname,
225
                    'rname' => $rname,
226
                    'serial' => $serial,
227
                    'refresh' => $refresh,
228
                    'retry' => $retry,
229
                    'expire' => $expire,
230
                    'minimum' => $minimum
231
                );
232
            }
233
        } elseif (Message::TYPE_OPT === $type) {
234
            $rdata = array();
235
            while (isset($message->data[$consumed + 4 - 1])) {
236
                list($code, $length) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
237
                $value = (string) substr($message->data, $consumed + 4, $length);
238
                if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') {
239
                    $value = null;
240
                } elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) {
241
                    list($value) = array_values(unpack('n', $value));
242
                    $value = round($value * 0.1, 1);
243
                } elseif ($code === Message::OPT_TCP_KEEPALIVE) {
244
                    break;
245
                }
246
                $rdata[$code] = $value;
247
                $consumed += 4 + $length;
248
            }
249
        } elseif (Message::TYPE_CAA === $type) {
250
            if ($rdLength > 3) {
251
                list($flag, $tagLength) = array_values(unpack('C*', substr($message->data, $consumed, 2)));
252
 
253
                if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) {
254
                    $tag = substr($message->data, $consumed + 2, $tagLength);
255
                    $value = substr($message->data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
256
                    $consumed += $rdLength;
257
 
258
                    $rdata = array(
259
                        'flag' => $flag,
260
                        'tag' => $tag,
261
                        'value' => $value
262
                    );
263
                }
264
            }
265
        } else {
266
            // unknown types simply parse rdata as an opaque binary string
267
            $rdata = substr($message->data, $consumed, $rdLength);
268
            $consumed += $rdLength;
269
        }
270
 
271
        // ensure parsing record data consumes expact number of bytes indicated in record length
272
        if ($consumed !== $expected || $rdata === null) {
273
            return null;
274
        }
275
 
276
        $message->consumed = $consumed;
277
 
278
        return new Record($name, $type, $class, $ttl, $rdata);
279
    }
280
 
281
    private function readDomain($data, $consumed)
282
    {
283
        list ($labels, $consumed) = $this->readLabels($data, $consumed);
284
 
285
        if ($labels === null) {
286
            return array(null, null);
287
        }
288
 
289
        // use escaped notation for each label part, then join using dots
290
        return array(
291
            \implode(
292
                '.',
293
                \array_map(
294
                    function ($label) {
295
                        return \addcslashes($label, "\0..\40.\177");
296
                    },
297
                    $labels
298
                )
299
            ),
300
            $consumed
301
        );
302
    }
303
 
304
    private function readLabels($data, $consumed)
305
    {
306
        $labels = array();
307
 
308
        while (true) {
309
            if (!isset($data[$consumed])) {
310
                return array(null, null);
311
            }
312
 
313
            $length = \ord($data[$consumed]);
314
 
315
            // end of labels reached
316
            if ($length === 0) {
317
                $consumed += 1;
318
                break;
319
            }
320
 
321
            // first two bits set? this is a compressed label (14 bit pointer offset)
322
            if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1])) {
323
                $offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]);
324
                if ($offset >= $consumed) {
325
                    return array(null, null);
326
                }
327
 
328
                $consumed += 2;
329
                list($newLabels) = $this->readLabels($data, $offset);
330
 
331
                if ($newLabels === null) {
332
                    return array(null, null);
333
                }
334
 
335
                $labels = array_merge($labels, $newLabels);
336
                break;
337
            }
338
 
339
            // length MUST be 0-63 (6 bits only) and data has to be large enough
340
            if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) {
341
                return array(null, null);
342
            }
343
 
344
            $labels[] = substr($data, $consumed + 1, $length);
345
            $consumed += $length + 1;
346
        }
347
 
348
        return array($labels, $consumed);
349
    }
350
}