TcpTransportExecutor.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. <?php
  2. namespace React\Dns\Query;
  3. use React\Dns\Model\Message;
  4. use React\Dns\Protocol\BinaryDumper;
  5. use React\Dns\Protocol\Parser;
  6. use React\EventLoop\Loop;
  7. use React\EventLoop\LoopInterface;
  8. use React\Promise\Deferred;
  9. /**
  10. * Send DNS queries over a TCP/IP stream transport.
  11. *
  12. * This is one of the main classes that send a DNS query to your DNS server.
  13. *
  14. * For more advanced usages one can utilize this class directly.
  15. * The following example looks up the `IPv6` address for `reactphp.org`.
  16. *
  17. * ```php
  18. * $executor = new TcpTransportExecutor('8.8.8.8:53');
  19. *
  20. * $executor->query(
  21. * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
  22. * )->then(function (Message $message) {
  23. * foreach ($message->answers as $answer) {
  24. * echo 'IPv6: ' . $answer->data . PHP_EOL;
  25. * }
  26. * }, 'printf');
  27. * ```
  28. *
  29. * See also [example #92](examples).
  30. *
  31. * Note that this executor does not implement a timeout, so you will very likely
  32. * want to use this in combination with a `TimeoutExecutor` like this:
  33. *
  34. * ```php
  35. * $executor = new TimeoutExecutor(
  36. * new TcpTransportExecutor($nameserver),
  37. * 3.0
  38. * );
  39. * ```
  40. *
  41. * Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP
  42. * transport, so you do not necessarily have to implement any retry logic.
  43. *
  44. * Note that this executor is entirely async and as such allows you to execute
  45. * queries concurrently. The first query will establish a TCP/IP socket
  46. * connection to the DNS server which will be kept open for a short period.
  47. * Additional queries will automatically reuse this existing socket connection
  48. * to the DNS server, will pipeline multiple requests over this single
  49. * connection and will keep an idle connection open for a short period. The
  50. * initial TCP/IP connection overhead may incur a slight delay if you only send
  51. * occasional queries – when sending a larger number of concurrent queries over
  52. * an existing connection, it becomes increasingly more efficient and avoids
  53. * creating many concurrent sockets like the UDP-based executor. You may still
  54. * want to limit the number of (concurrent) queries in your application or you
  55. * may be facing rate limitations and bans on the resolver end. For many common
  56. * applications, you may want to avoid sending the same query multiple times
  57. * when the first one is still pending, so you will likely want to use this in
  58. * combination with a `CoopExecutor` like this:
  59. *
  60. * ```php
  61. * $executor = new CoopExecutor(
  62. * new TimeoutExecutor(
  63. * new TcpTransportExecutor($nameserver),
  64. * 3.0
  65. * )
  66. * );
  67. * ```
  68. *
  69. * > Internally, this class uses PHP's TCP/IP sockets and does not take advantage
  70. * of [react/socket](https://github.com/reactphp/socket) purely for
  71. * organizational reasons to avoid a cyclic dependency between the two
  72. * packages. Higher-level components should take advantage of the Socket
  73. * component instead of reimplementing this socket logic from scratch.
  74. */
  75. class TcpTransportExecutor implements ExecutorInterface
  76. {
  77. private $nameserver;
  78. private $loop;
  79. private $parser;
  80. private $dumper;
  81. /**
  82. * @var ?resource
  83. */
  84. private $socket;
  85. /**
  86. * @var Deferred[]
  87. */
  88. private $pending = array();
  89. /**
  90. * @var string[]
  91. */
  92. private $names = array();
  93. /**
  94. * Maximum idle time when socket is current unused (i.e. no pending queries outstanding)
  95. *
  96. * If a new query is to be sent during the idle period, we can reuse the
  97. * existing socket without having to wait for a new socket connection.
  98. * This uses a rather small, hard-coded value to not keep any unneeded
  99. * sockets open and to not keep the loop busy longer than needed.
  100. *
  101. * A future implementation may take advantage of `edns-tcp-keepalive` to keep
  102. * the socket open for longer periods. This will likely require explicit
  103. * configuration because this may consume additional resources and also keep
  104. * the loop busy for longer than expected in some applications.
  105. *
  106. * @var float
  107. * @link https://tools.ietf.org/html/rfc7766#section-6.2.1
  108. * @link https://tools.ietf.org/html/rfc7828
  109. */
  110. private $idlePeriod = 0.001;
  111. /**
  112. * @var ?\React\EventLoop\TimerInterface
  113. */
  114. private $idleTimer;
  115. private $writeBuffer = '';
  116. private $writePending = false;
  117. private $readBuffer = '';
  118. private $readPending = false;
  119. /** @var string */
  120. private $readChunk = 0xffff;
  121. /**
  122. * @param string $nameserver
  123. * @param ?LoopInterface $loop
  124. */
  125. public function __construct($nameserver, LoopInterface $loop = null)
  126. {
  127. if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) {
  128. // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets
  129. $nameserver = '[' . $nameserver . ']';
  130. }
  131. $parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver);
  132. if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || @\inet_pton(\trim($parts['host'], '[]')) === false) {
  133. throw new \InvalidArgumentException('Invalid nameserver address given');
  134. }
  135. $this->nameserver = 'tcp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53);
  136. $this->loop = $loop ?: Loop::get();
  137. $this->parser = new Parser();
  138. $this->dumper = new BinaryDumper();
  139. }
  140. public function query(Query $query)
  141. {
  142. $request = Message::createRequestForQuery($query);
  143. // keep shuffing message ID to avoid using the same message ID for two pending queries at the same time
  144. while (isset($this->pending[$request->id])) {
  145. $request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore
  146. }
  147. $queryData = $this->dumper->toBinary($request);
  148. $length = \strlen($queryData);
  149. if ($length > 0xffff) {
  150. return \React\Promise\reject(new \RuntimeException(
  151. 'DNS query for ' . $query->describe() . ' failed: Query too large for TCP transport'
  152. ));
  153. }
  154. $queryData = \pack('n', $length) . $queryData;
  155. if ($this->socket === null) {
  156. // create async TCP/IP connection (may take a while)
  157. $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT);
  158. if ($socket === false) {
  159. return \React\Promise\reject(new \RuntimeException(
  160. 'DNS query for ' . $query->describe() . ' failed: Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
  161. $errno
  162. ));
  163. }
  164. // set socket to non-blocking and wait for it to become writable (connection success/rejected)
  165. \stream_set_blocking($socket, false);
  166. if (\function_exists('stream_set_chunk_size')) {
  167. \stream_set_chunk_size($socket, $this->readChunk); // @codeCoverageIgnore
  168. }
  169. $this->socket = $socket;
  170. }
  171. if ($this->idleTimer !== null) {
  172. $this->loop->cancelTimer($this->idleTimer);
  173. $this->idleTimer = null;
  174. }
  175. // wait for socket to become writable to actually write out data
  176. $this->writeBuffer .= $queryData;
  177. if (!$this->writePending) {
  178. $this->writePending = true;
  179. $this->loop->addWriteStream($this->socket, array($this, 'handleWritable'));
  180. }
  181. $names =& $this->names;
  182. $that = $this;
  183. $deferred = new Deferred(function () use ($that, &$names, $request) {
  184. // remove from list of pending names, but remember pending query
  185. $name = $names[$request->id];
  186. unset($names[$request->id]);
  187. $that->checkIdle();
  188. throw new CancellationException('DNS query for ' . $name . ' has been cancelled');
  189. });
  190. $this->pending[$request->id] = $deferred;
  191. $this->names[$request->id] = $query->describe();
  192. return $deferred->promise();
  193. }
  194. /**
  195. * @internal
  196. */
  197. public function handleWritable()
  198. {
  199. if ($this->readPending === false) {
  200. $name = @\stream_socket_get_name($this->socket, true);
  201. if ($name === false) {
  202. // Connection failed? Check socket error if available for underlying errno/errstr.
  203. // @codeCoverageIgnoreStart
  204. if (\function_exists('socket_import_stream')) {
  205. $socket = \socket_import_stream($this->socket);
  206. $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
  207. $errstr = \socket_strerror($errno);
  208. } else {
  209. $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
  210. $errstr = 'Connection refused';
  211. }
  212. // @codeCoverageIgnoreEnd
  213. $this->closeError('Unable to connect to DNS server ' . $this->nameserver . ' (' . $errstr . ')', $errno);
  214. return;
  215. }
  216. $this->readPending = true;
  217. $this->loop->addReadStream($this->socket, array($this, 'handleRead'));
  218. }
  219. $errno = 0;
  220. $errstr = '';
  221. \set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
  222. // Match errstr from PHP's warning message.
  223. // fwrite(): Send of 327712 bytes failed with errno=32 Broken pipe
  224. \preg_match('/errno=(\d+) (.+)/', $error, $m);
  225. $errno = isset($m[1]) ? (int) $m[1] : 0;
  226. $errstr = isset($m[2]) ? $m[2] : $error;
  227. });
  228. $written = \fwrite($this->socket, $this->writeBuffer);
  229. \restore_error_handler();
  230. if ($written === false || $written === 0) {
  231. $this->closeError(
  232. 'Unable to send query to DNS server ' . $this->nameserver . ' (' . $errstr . ')',
  233. $errno
  234. );
  235. return;
  236. }
  237. if (isset($this->writeBuffer[$written])) {
  238. $this->writeBuffer = \substr($this->writeBuffer, $written);
  239. } else {
  240. $this->loop->removeWriteStream($this->socket);
  241. $this->writePending = false;
  242. $this->writeBuffer = '';
  243. }
  244. }
  245. /**
  246. * @internal
  247. */
  248. public function handleRead()
  249. {
  250. // read one chunk of data from the DNS server
  251. // any error is fatal, this is a stream of TCP/IP data
  252. $chunk = @\fread($this->socket, $this->readChunk);
  253. if ($chunk === false || $chunk === '') {
  254. $this->closeError('Connection to DNS server ' . $this->nameserver . ' lost');
  255. return;
  256. }
  257. // reassemble complete message by concatenating all chunks.
  258. $this->readBuffer .= $chunk;
  259. // response message header contains at least 12 bytes
  260. while (isset($this->readBuffer[11])) {
  261. // read response message length from first 2 bytes and ensure we have length + data in buffer
  262. list(, $length) = \unpack('n', $this->readBuffer);
  263. if (!isset($this->readBuffer[$length + 1])) {
  264. return;
  265. }
  266. $data = \substr($this->readBuffer, 2, $length);
  267. $this->readBuffer = (string)substr($this->readBuffer, $length + 2);
  268. try {
  269. $response = $this->parser->parseMessage($data);
  270. } catch (\Exception $e) {
  271. // reject all pending queries if we received an invalid message from remote server
  272. $this->closeError('Invalid message received from DNS server ' . $this->nameserver);
  273. return;
  274. }
  275. // reject all pending queries if we received an unexpected response ID or truncated response
  276. if (!isset($this->pending[$response->id]) || $response->tc) {
  277. $this->closeError('Invalid response message received from DNS server ' . $this->nameserver);
  278. return;
  279. }
  280. $deferred = $this->pending[$response->id];
  281. unset($this->pending[$response->id], $this->names[$response->id]);
  282. $deferred->resolve($response);
  283. $this->checkIdle();
  284. }
  285. }
  286. /**
  287. * @internal
  288. * @param string $reason
  289. * @param int $code
  290. */
  291. public function closeError($reason, $code = 0)
  292. {
  293. $this->readBuffer = '';
  294. if ($this->readPending) {
  295. $this->loop->removeReadStream($this->socket);
  296. $this->readPending = false;
  297. }
  298. $this->writeBuffer = '';
  299. if ($this->writePending) {
  300. $this->loop->removeWriteStream($this->socket);
  301. $this->writePending = false;
  302. }
  303. if ($this->idleTimer !== null) {
  304. $this->loop->cancelTimer($this->idleTimer);
  305. $this->idleTimer = null;
  306. }
  307. @\fclose($this->socket);
  308. $this->socket = null;
  309. foreach ($this->names as $id => $name) {
  310. $this->pending[$id]->reject(new \RuntimeException(
  311. 'DNS query for ' . $name . ' failed: ' . $reason,
  312. $code
  313. ));
  314. }
  315. $this->pending = $this->names = array();
  316. }
  317. /**
  318. * @internal
  319. */
  320. public function checkIdle()
  321. {
  322. if ($this->idleTimer === null && !$this->names) {
  323. $that = $this;
  324. $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) {
  325. $that->closeError('Idle timeout');
  326. });
  327. }
  328. }
  329. }