Parser.php 12 KB

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