friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Socket;
4
5use Evenement\EventEmitter;
6use React\EventLoop\Loop;
7use React\EventLoop\LoopInterface;
8use InvalidArgumentException;
9use RuntimeException;
10
11/**
12 * The `TcpServer` class implements the `ServerInterface` and
13 * is responsible for accepting plaintext TCP/IP connections.
14 *
15 * ```php
16 * $server = new React\Socket\TcpServer(8080);
17 * ```
18 *
19 * Whenever a client connects, it will emit a `connection` event with a connection
20 * instance implementing `ConnectionInterface`:
21 *
22 * ```php
23 * $server->on('connection', function (React\Socket\ConnectionInterface $connection) {
24 * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL;
25 * $connection->write('hello there!' . PHP_EOL);
26 * …
27 * });
28 * ```
29 *
30 * See also the `ServerInterface` for more details.
31 *
32 * @see ServerInterface
33 * @see ConnectionInterface
34 */
35final class TcpServer extends EventEmitter implements ServerInterface
36{
37 private $master;
38 private $loop;
39 private $listening = false;
40
41 /**
42 * Creates a plaintext TCP/IP socket server and starts listening on the given address
43 *
44 * This starts accepting new incoming connections on the given address.
45 * See also the `connection event` documented in the `ServerInterface`
46 * for more details.
47 *
48 * ```php
49 * $server = new React\Socket\TcpServer(8080);
50 * ```
51 *
52 * As above, the `$uri` parameter can consist of only a port, in which case the
53 * server will default to listening on the localhost address `127.0.0.1`,
54 * which means it will not be reachable from outside of this system.
55 *
56 * In order to use a random port assignment, you can use the port `0`:
57 *
58 * ```php
59 * $server = new React\Socket\TcpServer(0);
60 * $address = $server->getAddress();
61 * ```
62 *
63 * In order to change the host the socket is listening on, you can provide an IP
64 * address through the first parameter provided to the constructor, optionally
65 * preceded by the `tcp://` scheme:
66 *
67 * ```php
68 * $server = new React\Socket\TcpServer('192.168.0.1:8080');
69 * ```
70 *
71 * If you want to listen on an IPv6 address, you MUST enclose the host in square
72 * brackets:
73 *
74 * ```php
75 * $server = new React\Socket\TcpServer('[::1]:8080');
76 * ```
77 *
78 * If the given URI is invalid, does not contain a port, any other scheme or if it
79 * contains a hostname, it will throw an `InvalidArgumentException`:
80 *
81 * ```php
82 * // throws InvalidArgumentException due to missing port
83 * $server = new React\Socket\TcpServer('127.0.0.1');
84 * ```
85 *
86 * If the given URI appears to be valid, but listening on it fails (such as if port
87 * is already in use or port below 1024 may require root access etc.), it will
88 * throw a `RuntimeException`:
89 *
90 * ```php
91 * $first = new React\Socket\TcpServer(8080);
92 *
93 * // throws RuntimeException because port is already in use
94 * $second = new React\Socket\TcpServer(8080);
95 * ```
96 *
97 * Note that these error conditions may vary depending on your system and/or
98 * configuration.
99 * See the exception message and code for more details about the actual error
100 * condition.
101 *
102 * This class takes an optional `LoopInterface|null $loop` parameter that can be used to
103 * pass the event loop instance to use for this object. You can use a `null` value
104 * here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
105 * This value SHOULD NOT be given unless you're sure you want to explicitly use a
106 * given event loop instance.
107 *
108 * Optionally, you can specify [socket context options](https://www.php.net/manual/en/context.socket.php)
109 * for the underlying stream socket resource like this:
110 *
111 * ```php
112 * $server = new React\Socket\TcpServer('[::1]:8080', null, array(
113 * 'backlog' => 200,
114 * 'so_reuseport' => true,
115 * 'ipv6_v6only' => true
116 * ));
117 * ```
118 *
119 * Note that available [socket context options](https://www.php.net/manual/en/context.socket.php),
120 * their defaults and effects of changing these may vary depending on your system
121 * and/or PHP version.
122 * Passing unknown context options has no effect.
123 * The `backlog` context option defaults to `511` unless given explicitly.
124 *
125 * @param string|int $uri
126 * @param ?LoopInterface $loop
127 * @param array $context
128 * @throws InvalidArgumentException if the listening address is invalid
129 * @throws RuntimeException if listening on this address fails (already in use etc.)
130 */
131 public function __construct($uri, $loop = null, array $context = array())
132 {
133 if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
134 throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface');
135 }
136
137 $this->loop = $loop ?: Loop::get();
138
139 // a single port has been given => assume localhost
140 if ((string)(int)$uri === (string)$uri) {
141 $uri = '127.0.0.1:' . $uri;
142 }
143
144 // assume default scheme if none has been given
145 if (\strpos($uri, '://') === false) {
146 $uri = 'tcp://' . $uri;
147 }
148
149 // parse_url() does not accept null ports (random port assignment) => manually remove
150 if (\substr($uri, -2) === ':0') {
151 $parts = \parse_url(\substr($uri, 0, -2));
152 if ($parts) {
153 $parts['port'] = 0;
154 }
155 } else {
156 $parts = \parse_url($uri);
157 }
158
159 // ensure URI contains TCP scheme, host and port
160 if (!$parts || !isset($parts['scheme'], $parts['host'], $parts['port']) || $parts['scheme'] !== 'tcp') {
161 throw new \InvalidArgumentException(
162 'Invalid URI "' . $uri . '" given (EINVAL)',
163 \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
164 );
165 }
166
167 if (@\inet_pton(\trim($parts['host'], '[]')) === false) {
168 throw new \InvalidArgumentException(
169 'Given URI "' . $uri . '" does not contain a valid host IP (EINVAL)',
170 \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22)
171 );
172 }
173
174 $this->master = @\stream_socket_server(
175 $uri,
176 $errno,
177 $errstr,
178 \STREAM_SERVER_BIND | \STREAM_SERVER_LISTEN,
179 \stream_context_create(array('socket' => $context + array('backlog' => 511)))
180 );
181 if (false === $this->master) {
182 if ($errno === 0) {
183 // PHP does not seem to report errno, so match errno from errstr
184 // @link https://3v4l.org/3qOBl
185 $errno = SocketServer::errno($errstr);
186 }
187
188 throw new \RuntimeException(
189 'Failed to listen on "' . $uri . '": ' . $errstr . SocketServer::errconst($errno),
190 $errno
191 );
192 }
193 \stream_set_blocking($this->master, false);
194
195 $this->resume();
196 }
197
198 public function getAddress()
199 {
200 if (!\is_resource($this->master)) {
201 return null;
202 }
203
204 $address = \stream_socket_get_name($this->master, false);
205
206 // check if this is an IPv6 address which includes multiple colons but no square brackets
207 $pos = \strrpos($address, ':');
208 if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') {
209 $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore
210 }
211
212 return 'tcp://' . $address;
213 }
214
215 public function pause()
216 {
217 if (!$this->listening) {
218 return;
219 }
220
221 $this->loop->removeReadStream($this->master);
222 $this->listening = false;
223 }
224
225 public function resume()
226 {
227 if ($this->listening || !\is_resource($this->master)) {
228 return;
229 }
230
231 $that = $this;
232 $this->loop->addReadStream($this->master, function ($master) use ($that) {
233 try {
234 $newSocket = SocketServer::accept($master);
235 } catch (\RuntimeException $e) {
236 $that->emit('error', array($e));
237 return;
238 }
239 $that->handleConnection($newSocket);
240 });
241 $this->listening = true;
242 }
243
244 public function close()
245 {
246 if (!\is_resource($this->master)) {
247 return;
248 }
249
250 $this->pause();
251 \fclose($this->master);
252 $this->removeAllListeners();
253 }
254
255 /** @internal */
256 public function handleConnection($socket)
257 {
258 $this->emit('connection', array(
259 new Connection($socket, $this->loop)
260 ));
261 }
262}