TcpConnector.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. namespace React\Socket;
  3. use React\EventLoop\Loop;
  4. use React\EventLoop\LoopInterface;
  5. use React\Promise;
  6. use InvalidArgumentException;
  7. use RuntimeException;
  8. final class TcpConnector implements ConnectorInterface
  9. {
  10. private $loop;
  11. private $context;
  12. /**
  13. * @param ?LoopInterface $loop
  14. * @param array $context
  15. */
  16. public function __construct($loop = null, array $context = array())
  17. {
  18. if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
  19. throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
  20. }
  21. $this->loop = $loop ?: Loop::get();
  22. $this->context = $context;
  23. }
  24. public function connect($uri)
  25. {
  26. if (\strpos($uri, '://') === false) {
  27. $uri = 'tcp://' . $uri;
  28. }
  29. $parts = \parse_url($uri);
  30. if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
  31. return Promise\reject(new \InvalidArgumentException(
  32. 'Given URI "' . $uri . '" is invalid (EINVAL)',
  33. \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
  34. ));
  35. }
  36. $ip = \trim($parts['host'], '[]');
  37. if (@\inet_pton($ip) === false) {
  38. return Promise\reject(new \InvalidArgumentException(
  39. 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)',
  40. \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
  41. ));
  42. }
  43. // use context given in constructor
  44. $context = array(
  45. 'socket' => $this->context
  46. );
  47. // parse arguments from query component of URI
  48. $args = array();
  49. if (isset($parts['query'])) {
  50. \parse_str($parts['query'], $args);
  51. }
  52. // If an original hostname has been given, use this for TLS setup.
  53. // This can happen due to layers of nested connectors, such as a
  54. // DnsConnector reporting its original hostname.
  55. // These context options are here in case TLS is enabled later on this stream.
  56. // If TLS is not enabled later, this doesn't hurt either.
  57. if (isset($args['hostname'])) {
  58. $context['ssl'] = array(
  59. 'SNI_enabled' => true,
  60. 'peer_name' => $args['hostname']
  61. );
  62. // Legacy PHP < 5.6 ignores peer_name and requires legacy context options instead.
  63. // The SNI_server_name context option has to be set here during construction,
  64. // as legacy PHP ignores any values set later.
  65. // @codeCoverageIgnoreStart
  66. if (\PHP_VERSION_ID < 50600) {
  67. $context['ssl'] += array(
  68. 'SNI_server_name' => $args['hostname'],
  69. 'CN_match' => $args['hostname']
  70. );
  71. }
  72. // @codeCoverageIgnoreEnd
  73. }
  74. // latest versions of PHP no longer accept any other URI components and
  75. // HHVM fails to parse URIs with a query but no path, so let's simplify our URI here
  76. $remote = 'tcp://' . $parts['host'] . ':' . $parts['port'];
  77. $stream = @\stream_socket_client(
  78. $remote,
  79. $errno,
  80. $errstr,
  81. 0,
  82. \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT,
  83. \stream_context_create($context)
  84. );
  85. if (false === $stream) {
  86. return Promise\reject(new \RuntimeException(
  87. 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno),
  88. $errno
  89. ));
  90. }
  91. // wait for connection
  92. $loop = $this->loop;
  93. return new Promise\Promise(function ($resolve, $reject) use ($loop, $stream, $uri) {
  94. $loop->addWriteStream($stream, function ($stream) use ($loop, $resolve, $reject, $uri) {
  95. $loop->removeWriteStream($stream);
  96. // The following hack looks like the only way to
  97. // detect connection refused errors with PHP's stream sockets.
  98. if (false === \stream_socket_get_name($stream, true)) {
  99. // If we reach this point, we know the connection is dead, but we don't know the underlying error condition.
  100. // @codeCoverageIgnoreStart
  101. if (\function_exists('socket_import_stream')) {
  102. // actual socket errno and errstr can be retrieved with ext-sockets on PHP 5.4+
  103. $socket = \socket_import_stream($stream);
  104. $errno = \socket_get_option($socket, \SOL_SOCKET, \SO_ERROR);
  105. $errstr = \socket_strerror($errno);
  106. } elseif (\PHP_OS === 'Linux') {
  107. // Linux reports socket errno and errstr again when trying to write to the dead socket.
  108. // Suppress error reporting to get error message below and close dead socket before rejecting.
  109. // This is only known to work on Linux, Mac and Windows are known to not support this.
  110. $errno = 0;
  111. $errstr = '';
  112. \set_error_handler(function ($_, $error) use (&$errno, &$errstr) {
  113. // Match errstr from PHP's warning message.
  114. // fwrite(): send of 1 bytes failed with errno=111 Connection refused
  115. \preg_match('/errno=(\d+) (.+)/', $error, $m);
  116. $errno = isset($m[1]) ? (int) $m[1] : 0;
  117. $errstr = isset($m[2]) ? $m[2] : $error;
  118. });
  119. \fwrite($stream, \PHP_EOL);
  120. \restore_error_handler();
  121. } else {
  122. // Not on Linux and ext-sockets not available? Too bad.
  123. $errno = \defined('SOCKET_ECONNREFUSED') ? \SOCKET_ECONNREFUSED : 111;
  124. $errstr = 'Connection refused?';
  125. }
  126. // @codeCoverageIgnoreEnd
  127. \fclose($stream);
  128. $reject(new \RuntimeException(
  129. 'Connection to ' . $uri . ' failed: ' . $errstr . SocketServer::errconst($errno),
  130. $errno
  131. ));
  132. } else {
  133. $resolve(new Connection($stream, $loop));
  134. }
  135. });
  136. }, function () use ($loop, $stream, $uri) {
  137. $loop->removeWriteStream($stream);
  138. \fclose($stream);
  139. // @codeCoverageIgnoreStart
  140. // legacy PHP 5.3 sometimes requires a second close call (see tests)
  141. if (\PHP_VERSION_ID < 50400 && \is_resource($stream)) {
  142. \fclose($stream);
  143. }
  144. // @codeCoverageIgnoreEnd
  145. throw new \RuntimeException(
  146. 'Connection to ' . $uri . ' cancelled during TCP/IP handshake (ECONNABORTED)',
  147. \defined('SOCKET_ECONNABORTED') ? \SOCKET_ECONNABORTED : 103
  148. );
  149. });
  150. }
  151. }