/** * Coroutine that handles incoming WebSocket frames. */ protected function processIncomingFrames() : \Generator { $e = null; try { while (true) { $frame = (yield from $this->readNextFrame()); if ($frame->isControlFrame()) { if (!(yield from $this->handleControlFrame($frame))) { break; } } else { switch ($frame->opcode) { case Frame::TEXT: yield from $this->handleTextFrame($frame); break; case Frame::BINARY: yield from $this->handleBinaryFrame($frame); break; case Frame::CONTINUATION: yield from $this->handleContinuationFrame($frame); break; } } } } catch (\Throwable $e) { $this->messages->close($e); } finally { $this->messages->close(); try { foreach ($this->pings as $defer) { $defer->fail($e ?? new \RuntimeException('WebSocket connection closed')); } } finally { $this->pings = []; } try { if ($this->socket->isAlive()) { $reason = $e === null || $e->getCode() === 0 ? Frame::NORMAL_CLOSURE : $e->getCode(); (yield $this->writer->sendFrame(new Frame(Frame::CONNECTION_CLOSE, \pack('n', $reason)))); } } finally { if ($this->logger) { $this->logger->debug('WebSocket connection to {peer} closed', ['peer' => $this->socket->getRemoteAddress()]); } $this->socket->close(); } } }
/** * {@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); }
/** * 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]); } } } }
/** * {@inheritdoc} */ public function handleConnection(HttpDriverContext $context, SocketStream $socket, callable $action) : Awaitable { return new Coroutine(function () use($context, $socket, $action) { $remotePeer = $socket->getRemoteAddress(); $upgraded = false; if ($this->logger) { $this->logger->debug('Accepted new HTTP/1 connection from {peer}', ['peer' => $remotePeer]); } try { $request = (yield from $this->parseNextRequest($context, $socket)); if ($request->getProtocolVersion() !== '1.0' && $request->hasHeader('Host')) { try { yield from $this->upgradeConnection($context, $socket, $request, $action, $upgraded); if ($upgraded) { return; } } catch (\Throwable $e) { return yield from $this->sendErrorResponse($socket, $request, $e); } } $tokens = $request->getHeaderTokenValues('Connection'); if (\in_array('upgrade', $tokens, true)) { foreach ($tokens as $i => $token) { if ($token === 'keep-alive') { unset($tokens[$i]); } } // Ensure connections with an upgrade token in the connection header are not pipelined / persistent. $request = $request->withHeader('Connection', \implode(', ', \array_merge($tokens, ['close']))); } $pipeline = Channel::fromGenerator(10, function (Channel $channel) use($socket, $request, $context) { yield from $this->parseIncomingRequests($context, $socket, $channel, $request); }); try { while (null !== ($next = (yield $pipeline->receive()))) { if (!(yield from $this->processRequest($context, $socket, $action, $upgraded, ...$next))) { break; } } $pipeline->close(); } catch (\Throwable $e) { $pipeline->close($e); } } finally { try { $socket->close(); } finally { if ($this->logger && !$upgraded) { $this->logger->debug('Closed HTTP/1 connection to {peer}', ['peer' => $remotePeer]); } } } }); }
/** * 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]); } } }