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; 8 9/** 10 * [Internal] The `FdServer` class implements the `ServerInterface` and 11 * is responsible for accepting connections from an existing file descriptor. 12 * 13 * ```php 14 * $socket = new React\Socket\FdServer(3); 15 * ``` 16 * 17 * Whenever a client connects, it will emit a `connection` event with a connection 18 * instance implementing `ConnectionInterface`: 19 * 20 * ```php 21 * $socket->on('connection', function (ConnectionInterface $connection) { 22 * echo 'Plaintext connection from ' . $connection->getRemoteAddress() . PHP_EOL; 23 * $connection->write('hello there!' . PHP_EOL); 24 * … 25 * }); 26 * ``` 27 * 28 * See also the `ServerInterface` for more details. 29 * 30 * @see ServerInterface 31 * @see ConnectionInterface 32 * @internal 33 */ 34final class FdServer extends EventEmitter implements ServerInterface 35{ 36 private $master; 37 private $loop; 38 private $unix = false; 39 private $listening = false; 40 41 /** 42 * Creates a socket server and starts listening on the given file descriptor 43 * 44 * This starts accepting new incoming connections on the given file descriptor. 45 * See also the `connection event` documented in the `ServerInterface` 46 * for more details. 47 * 48 * ```php 49 * $socket = new React\Socket\FdServer(3); 50 * ``` 51 * 52 * If the given FD is invalid or out of range, it will throw an `InvalidArgumentException`: 53 * 54 * ```php 55 * // throws InvalidArgumentException 56 * $socket = new React\Socket\FdServer(-1); 57 * ``` 58 * 59 * If the given FD appears to be valid, but listening on it fails (such as 60 * if the FD does not exist or does not refer to a socket server), it will 61 * throw a `RuntimeException`: 62 * 63 * ```php 64 * // throws RuntimeException because FD does not reference a socket server 65 * $socket = new React\Socket\FdServer(0, $loop); 66 * ``` 67 * 68 * Note that these error conditions may vary depending on your system and/or 69 * configuration. 70 * See the exception message and code for more details about the actual error 71 * condition. 72 * 73 * @param int|string $fd FD number such as `3` or as URL in the form of `php://fd/3` 74 * @param ?LoopInterface $loop 75 * @throws \InvalidArgumentException if the listening address is invalid 76 * @throws \RuntimeException if listening on this address fails (already in use etc.) 77 */ 78 public function __construct($fd, $loop = null) 79 { 80 if (\preg_match('#^php://fd/(\d+)$#', $fd, $m)) { 81 $fd = (int) $m[1]; 82 } 83 if (!\is_int($fd) || $fd < 0 || $fd >= \PHP_INT_MAX) { 84 throw new \InvalidArgumentException( 85 'Invalid FD number given (EINVAL)', 86 \defined('SOCKET_EINVAL') ? \SOCKET_EINVAL : (\defined('PCNTL_EINVAL') ? \PCNTL_EINVAL : 22) 87 ); 88 } 89 90 if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1 91 throw new \InvalidArgumentException('Argument #2 ($loop) expected null|React\EventLoop\LoopInterface'); 92 } 93 94 $this->loop = $loop ?: Loop::get(); 95 96 $errno = 0; 97 $errstr = ''; 98 \set_error_handler(function ($_, $error) use (&$errno, &$errstr) { 99 // Match errstr from PHP's warning message. 100 // fopen(php://fd/3): Failed to open stream: Error duping file descriptor 3; possibly it doesn't exist: [9]: Bad file descriptor 101 \preg_match('/\[(\d+)\]: (.*)/', $error, $m); 102 $errno = isset($m[1]) ? (int) $m[1] : 0; 103 $errstr = isset($m[2]) ? $m[2] : $error; 104 }); 105 106 $this->master = \fopen('php://fd/' . $fd, 'r+'); 107 108 \restore_error_handler(); 109 110 if (false === $this->master) { 111 throw new \RuntimeException( 112 'Failed to listen on FD ' . $fd . ': ' . $errstr . SocketServer::errconst($errno), 113 $errno 114 ); 115 } 116 117 $meta = \stream_get_meta_data($this->master); 118 if (!isset($meta['stream_type']) || $meta['stream_type'] !== 'tcp_socket') { 119 \fclose($this->master); 120 121 $errno = \defined('SOCKET_ENOTSOCK') ? \SOCKET_ENOTSOCK : 88; 122 $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Not a socket'; 123 124 throw new \RuntimeException( 125 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (ENOTSOCK)', 126 $errno 127 ); 128 } 129 130 // Socket should not have a peer address if this is a listening socket. 131 // Looks like this work-around is the closest we can get because PHP doesn't expose SO_ACCEPTCONN even with ext-sockets. 132 if (\stream_socket_get_name($this->master, true) !== false) { 133 \fclose($this->master); 134 135 $errno = \defined('SOCKET_EISCONN') ? \SOCKET_EISCONN : 106; 136 $errstr = \function_exists('socket_strerror') ? \socket_strerror($errno) : 'Socket is connected'; 137 138 throw new \RuntimeException( 139 'Failed to listen on FD ' . $fd . ': ' . $errstr . ' (EISCONN)', 140 $errno 141 ); 142 } 143 144 // Assume this is a Unix domain socket (UDS) when its listening address doesn't parse as a valid URL with a port. 145 // Looks like this work-around is the closest we can get because PHP doesn't expose SO_DOMAIN even with ext-sockets. 146 $this->unix = \parse_url($this->getAddress(), \PHP_URL_PORT) === false; 147 148 \stream_set_blocking($this->master, false); 149 150 $this->resume(); 151 } 152 153 public function getAddress() 154 { 155 if (!\is_resource($this->master)) { 156 return null; 157 } 158 159 $address = \stream_socket_get_name($this->master, false); 160 161 if ($this->unix === true) { 162 return 'unix://' . $address; 163 } 164 165 // check if this is an IPv6 address which includes multiple colons but no square brackets 166 $pos = \strrpos($address, ':'); 167 if ($pos !== false && \strpos($address, ':') < $pos && \substr($address, 0, 1) !== '[') { 168 $address = '[' . \substr($address, 0, $pos) . ']:' . \substr($address, $pos + 1); // @codeCoverageIgnore 169 } 170 171 return 'tcp://' . $address; 172 } 173 174 public function pause() 175 { 176 if (!$this->listening) { 177 return; 178 } 179 180 $this->loop->removeReadStream($this->master); 181 $this->listening = false; 182 } 183 184 public function resume() 185 { 186 if ($this->listening || !\is_resource($this->master)) { 187 return; 188 } 189 190 $that = $this; 191 $this->loop->addReadStream($this->master, function ($master) use ($that) { 192 try { 193 $newSocket = SocketServer::accept($master); 194 } catch (\RuntimeException $e) { 195 $that->emit('error', array($e)); 196 return; 197 } 198 $that->handleConnection($newSocket); 199 }); 200 $this->listening = true; 201 } 202 203 public function close() 204 { 205 if (!\is_resource($this->master)) { 206 return; 207 } 208 209 $this->pause(); 210 \fclose($this->master); 211 $this->removeAllListeners(); 212 } 213 214 /** @internal */ 215 public function handleConnection($socket) 216 { 217 $connection = new Connection($socket, $this->loop); 218 $connection->unix = $this->unix; 219 220 $this->emit('connection', array($connection)); 221 } 222}