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}