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}