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