/** * Coroutine that connects to a WebSocket server. * * @param Uri $uri HTTP URI (use http / https as scheme instead of ws / wss). * @param array $options Stream context options (e.g. SSL settings) to be passed to SocketStream::connect(). * @param LoggerInterface $logger * @return WebSocketConnector */ public static function connect(Uri $uri, array $options = [], LoggerInterface $logger = NULL) : \Generator { $secure = $uri->getScheme() === 'https'; $host = $uri->getHost(); $port = $uri->getPort() ?? ($secure ? 443 : 80); $socket = (yield from SocketStream::connect($host, $port, 'tcp', 0, array_replace_recursive($options, ['socket' => ['tcp_nodelay' => true]]))); try { if ($secure) { yield from $socket->encryptClient(); } $nonce = base64_encode(random_bytes(16)); $message = sprintf("GET /%s HTTP/1.1\r\n", $uri->getPath()); $message .= "Upgrade: websocket\r\n"; $message .= "Connection: Upgrade\r\n"; $message .= "Content-Length: 0\r\n"; $message .= sprintf("Host: %s\r\n", $host); $message .= sprintf("Sec-WebSocket-Key: %s\r\n", $nonce); $message .= "Sec-WebSocket-Version: 13\r\n"; $message .= "\r\n"; yield from $socket->write($message); $m = NULL; $line = (yield from $socket->readLine()); if (!preg_match("'^HTTP/(1\\.[01])\\s+([1-5][0-9]{2})\\s*(.*)\$'i", $line, $m)) { throw new WebSocketException('Server did not respond with a valid HTTP/1 status line'); } $response = new HttpResponse(); $response = $response->withStatus((int) $m[2], trim($m[3])); $response = $response->withProtocolVersion($m[1]); while (!$socket->eof()) { $line = (yield from $socket->readLine()); if ($line === '') { break; } $header = array_map('trim', explode(':', $line, 2)); $response = $response->withAddedHeader(...$header); } if ($response->getProtocolVersion() !== '1.1') { throw new WebSocketException('WebSocket handshake requires HTTP/1.1 response'); } if ($response->getStatusCode() !== 101) { throw new WebSocketException('WebSocket protocol switching failed'); } $accept = base64_encode(sha1($nonce . self::GUID, true)); if ($accept !== $response->getHeaderLine('Sec-WebSocket-Accept')) { throw new WebSocketException('Invalid WebSocket Accept header received'); } } catch (\Throwable $e) { $socket->close(); throw $e; } $conn = new static($socket, (yield eventEmitter()), $logger); $conn->watcher = (yield runTask($conn->handleFrames(), 'WebSocket Message Handler')); $conn->watcher->setAutoShutdown(true); return $conn; }
/** * Reads the next WebSocket frame from the socket. * * This method will unmask frames as needed and asserts frame size constraints. * * @return Frame */ protected function readNextFrame() : \Generator { list($byte1, $byte2) = \array_map('ord', \str_split((yield $this->socket->readBuffer(2, true)), 1)); $masked = $byte2 & Frame::MASKED ? true : false; if ($this->client && $masked) { throw new ConnectionException('Received masked frame from server', Frame::PROTOCOL_ERROR); } if (!$this->client && !$masked) { throw new ConnectionException('Received unmasked frame from client', Frame::PROTOCOL_ERROR); } // Parse extended length fields: $len = $byte2 & Frame::LENGTH; if ($len === 0x7e) { $len = \unpack('n', (yield $this->socket->readBuffer(2, true)))[1]; } elseif ($len === 0x7f) { $lp = \unpack('N2', (yield $this->socket->readBuffer(8, true))); // 32 bit int check: if (\PHP_INT_MAX === 0x7fffffff) { if ($lp[1] !== 0 || $lp[2] < 0) { throw new ConnectionException('Max payload size exceeded', Frame::MESSAGE_TOO_BIG); } $len = $lp[2]; } else { $len = $lp[1] << 32 | $lp[2]; if ($len < 0) { throw new ConnectionException('Cannot use most significant bit in 64 bit length field', Frame::MESSAGE_TOO_BIG); } } } if ($len < 0) { throw new ConnectionException('Payload length must not be negative', Frame::MESSAGE_TOO_BIG); } if ($len > $this->maxFrameSize) { throw new ConnectionException(\sprintf('Maximum frame size of %u bytes exceeded', $this->maxFrameSize), Frame::MESSAGE_TOO_BIG); } // Read and unmask frame data. if ($this->client) { $data = (yield $this->socket->readBuffer($len, true)); } else { $key = (yield $this->socket->readBuffer(4, true)); $data = (yield $this->socket->readBuffer($len, true)) ^ \str_pad($key, $len, $key, \STR_PAD_RIGHT); } return new Frame($byte1 & Frame::OPCODE, $data, $byte1 & Frame::FINISHED ? true : false, $byte1 & Frame::RESERVED); }
public static function connect(string $dsn, string $username, string $password, LoggerInterface $logger = NULL) : \Generator { if ('mysql:' !== substr($dsn, 0, 6)) { throw new \InvalidArgumentException(sprintf('Invalid MySQL DSN: "%s"', $dsn)); } $settings = []; foreach (explode(';', substr($dsn, 6)) as $part) { list($k, $v) = array_map('trim', explode('=', $part)); switch ($k) { case 'host': case 'dbname': case 'unix_socket': $settings[$k] = $v; break; case 'port': $settings[$k] = (int) $v; break; default: throw new \InvalidArgumentException(sprintf('Unknown MySQL DSN param: "%s"', $k)); } } if (empty($settings['host']) && empty($settings['unix_socket'])) { throw new \InvalidArgumentException('Neighter MySQL host nor Unix domain socket specified in MySQL DSN'); } if (!empty($settings['unix_socket'])) { if (!Socket::isUnixSocketSupported()) { throw new \RuntimeException(sprintf('Cannot connect to MySQL socket "%s", PHP was not compiled with support for Unix domain sockets', $settings['unix_socket'])); } $client = new Client(yield from SocketStream::fromUrl('unix://' . $settings['unix_socket']), $logger); } else { $client = new Client(yield from SocketStream::connect($settings['host'], $settings['port'] ?? 3306), $logger); } yield from $client->handleHandshake($username, $password); $conn = new static($client, (yield currentExecutor()), $logger); if (!empty($settings['dbname'])) { yield from $conn->changeDefaultSchema($settings['dbname']); } return $conn; }
/** * {@inheritdoc} */ public function upgradeConnection(SocketStream $socket, HttpRequest $request, HttpResponse $response) : \Generator { $endpoint = $response->getAttribute(Endpoint::class); if (!$endpoint instanceof Endpoint) { throw new \InvalidArgumentException('No endpoint object passed to WebSocket handler'); } if ($this->logger) { $this->logger->debug('HTTP/{protocol} connection from {peer} upgraded to WebSocket', ['protocol' => $request->getProtocolVersion(), 'peer' => $socket->getRemoteAddress()]); } $conn = new Connection($socket, false, $response->getHeaderLine('Sec-WebSocket-Protocol')); if ($this->logger) { $conn->setLogger($this->logger); } if ($deflate = $response->getAttribute(PerMessageDeflate::class)) { $conn->enablePerMessageDeflate($deflate); } yield from $this->delegateToEndpoint($conn, $endpoint); }
private static function handshake(SocketStream $socket) : \Generator { $transmitter = new SocketTransmitter($socket); list($type, $data) = (yield from $transmitter->receive()); if ($type !== SocketTransmitter::TYPE_HANDSHAKE || !isset($data['id'])) { return $socket->close(); } $id = $data['id']; if (empty(self::$handshakes[$id])) { return $socket->close(); } list($worker, $defer) = self::$handshakes[$id]; unset(self::$handshakes[$id]); if (empty(self::$handshakes) && self::$serverAwait !== null) { self::$serverAwait->cancel(new PoolShutdownException('Handshakes done')); } $worker->connect($socket, $transmitter); self::$sharedWorkers[$id] = $worker; $defer->resolve($worker); }
/** * Write the given frame to the socket (does not honor concurrency control via writer). * * @param Frame $frame * @return int Number of transmitted bytes. */ public function writeFrame(Frame $frame) : Awaitable { return $this->socket->write($frame->encode($this->client ? \random_bytes(4) : null)); }
/** * Perform a direct upgrade of the connection to HTTP/2. * * @param HttpDriverContext $context HTTP context related to the HTTP endpoint. * @param SocketStream $socket The underlying socket transport. * @param HttpRequest $request The HTTP request that caused the connection upgrade. * @param callable $action Server action to be performed for each incoming HTTP request. */ protected function upgradeConnectionDirect(HttpDriverContext $context, SocketStream $socket, HttpRequest $request, callable $action) : \Generator { $preface = (yield $socket->readBuffer(\strlen(Connection::PREFACE_BODY), true)); if ($preface !== Connection::PREFACE_BODY) { throw new StatusException(Http::BAD_REQUEST, 'Invalid HTTP/2 connection preface body'); } if ($this->logger) { $this->logger->info('{ip} "{method} {target} HTTP/{protocol}" {status} {size}', ['ip' => $request->getClientAddress(), 'method' => $request->getMethod(), 'target' => $request->getRequestTarget(), 'protocol' => $request->getProtocolVersion(), 'status' => Http::SWITCHING_PROTOCOLS, 'size' => '-']); } $conn = new Connection($socket, new HPack($this->hpackContext), $this->logger); (yield $conn->performServerHandshake(null, true)); if ($this->logger) { $this->logger->info('HTTP/{protocol} connection from {peer} upgraded to HTTP/2', ['protocol' => $request->getProtocolVersion(), 'peer' => $socket->getRemoteAddress()]); } $remotePeer = $socket->getRemoteAddress(); try { while (null !== ($received = (yield $conn->nextRequest($context)))) { new Coroutine($this->processRequest($conn, $action, ...$received), true); } } finally { try { $conn->shutdown(); } finally { if ($this->logger) { $this->logger->debug('Closed HTTP/2 connection to {peer}', ['peer' => $remotePeer]); } } } }
protected function sendRequest(SocketStream $socket, HttpRequest $request, int &$sent) : \Generator { $request = $this->normalizeRequest($request); $body = $request->getBody(); $size = (yield $body->getSize()); $sendfile = false; if ($body instanceof FileBody && $socket->isSendfileSupported()) { $sendfile = true; $chunk = $size ? '' : null; } else { if ($request->getProtocolVersion() == '1.0' && $size === null) { if (!$body->isCached()) { $body = new BufferedBody((yield $body->getReadableStream())); } (yield $body->discard()); $size = (yield $body->getSize()); } $bodyStream = (yield $body->getReadableStream()); $clen = $size === null ? 4089 : 4096; $chunk = (yield $bodyStream->readBuffer($clen)); $len = \strlen($chunk ?? ''); if ($chunk === null) { $size = 0; } elseif ($len < $clen) { $size = $len; } } $buffer = $this->serializeHeaders($request, $size); $expect = false; if ($this->expectContinue && $chunk !== null && $request->getProtocolVersion() == '1.1') { $expect = true; $buffer .= "Expect: 100-continue\r\n"; } (yield $socket->write($buffer . "\r\n")); (yield $socket->flush()); if ($expect) { if (!\preg_match("'^HTTP/1\\.1\\s+100(?:\$|\\s)'i", $line = (yield $socket->readLine()))) { try { return $line; } finally { if (isset($bodyStream)) { $bodyStream->close(); } } } } if ($sendfile) { if ($size) { $sent += (yield LoopConfig::currentFilesystem()->sendfile($body->getFile(), $socket->getSocket(), $size)); } } elseif ($size === null) { $sent += (yield $socket->write(\dechex($len) . "\r\n" . $chunk . "\r\n")); if ($len === $clen) { // Align each chunk with length and line breaks to fit into 4 KB payload. $sent += (yield new CopyBytes($bodyStream, $socket, true, null, 4089, function (string $chunk) { return \dechex(\strlen($chunk)) . "\r\n" . $chunk . "\r\n"; })); } (yield $socket->write("0\r\n\r\n")); } elseif ($size > 0) { $sent += (yield $socket->write($chunk)); if ($len === $clen) { $sent += (yield new CopyBytes($bodyStream, $socket, true, $size - $len)); } } (yield $socket->flush()); }
protected function sendDeferredResponse(SocketStream $socket, HttpRequest $request, HttpResponse $response, bool $nobody, bool $close) : \Generator { if ($this->logger) { $this->logger->info('{ip} "{method} {target} HTTP/{protocol}" {status} {size}', ['ip' => $request->getClientAddress(), 'method' => $request->getMethod(), 'target' => $request->getRequestTarget(), 'protocol' => $request->getProtocolVersion(), 'status' => $response->getStatusCode(), 'size' => '-']); } if (!$nobody) { $close = true; } (yield $socket->write($this->serializeHeaders($response, $close, null, $nobody, true) . "\r\n")); (yield $socket->flush()); if ($nobody) { $response->getBody()->close(false); return !$close; } $watcher = null; $task = new Coroutine(function () use(&$watcher, $socket, $request, $response) { $body = $response->getBody(); $body->start($request, $this->logger); $bodyStream = (yield $body->getReadableStream()); $e = null; try { while (null !== ($chunk = (yield $bodyStream->read()))) { (yield $socket->write($chunk)); } } catch (StreamClosedException $e) { // Client disconnected from server. } finally { try { $bodyStream->close(); } finally { $body->close($e ? true : false); } $watcher->cancel(new \RuntimeException()); } return false; }, true); $watcher = new Coroutine(function () use($socket, $task) { $socket = $socket->getSocket(); while (\is_resource($socket) && !\feof($socket)) { (yield new AwaitRead($socket)); } $task->cancel(new StreamClosedException('Client disconnected')); }); return (yield $task); }
/** * Coroutine that processes inbound FCGI records. */ protected function handleIncomingRecords() : \Generator { static $header = 'Cversion/Ctype/nid/nlen/Cpad/x'; try { $peer = $this->socket->getRemoteAddress(); if ($this->logger) { $this->logger->debug('Accepted new FCGI connection from {peer}', ['peer' => $peer]); } while (true) { list($version, $type, $id, $len, $pad) = \array_values(\unpack($header, (yield $this->socket->readBuffer(8, true)))); $payload = $len > 0 ? (yield $this->socket->readBuffer($len, true)) : ''; if ($pad > 0) { (yield $this->socket->readBuffer($pad, true)); } $record = new Record($version, $type, $id, $payload); switch ($record->type) { case Record::FCGI_BEGIN_REQUEST: list($role, $flags) = \array_values(\unpack('nrole/Cflags/x5', $record->data)); if ($role != self::FCGI_RESPONDER) { throw new \RuntimeException('Unsupported FGCI role'); } $this->handlers[$id] = new Handler($id, $this, $this->context, $flags & self::FCGI_KEEP_CONNECTION ? true : false); if ($this->logger) { $this->handlers[$id]->setLogger($this->logger); } break; case Record::FCGI_ABORT_REQUEST: if (!(yield $this->closeHandler($id))) { return; } break; case Record::FCGI_PARAMS: $this->handlers[$id]->handleParams($record); break; case Record::FCGI_STDIN: yield from $this->handlers[$id]->handleStdin($record, $this->incoming); break; } } } finally { $this->socket->close(); $this->processor = null; if ($this->logger) { $this->logger->debug('Closed FCGi connection to {peer}', ['peer' => $peer]); } } }