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