/** * 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); }
/** * 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]); } } } }
/** * 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]); } } }