/** * Parse the next HTTP response from the given stream. * * @param ReadableStream $stream * @param string $line HTTP response line (mighty already be received while checking for 100 continue). * @param bool $dropBody Drop response body (needed when the response is caused by a HEAD request). * @return HttpResponse * * @throws StreamClosedException When no HTTP response line could be parsed. */ public function parseResponse(ReadableStream $stream, string $line = null, bool $dropBody = false) : \Generator { if ($line === null) { $i = 0; do { if ($i++ > 3 || null === ($line = (yield $stream->readLine()))) { throw new StreamClosedException('Stream closed before HTTP response line was read'); } } while ($line === ''); } $m = null; if (!\preg_match("'^HTTP/(1\\.[01])\\s+([1-5][0-9]{2})(.*)\$'i", \trim($line), $m)) { throw new StreamClosedException('Invalid HTTP response line received'); } $response = new HttpResponse(); $response = $response->withProtocolVersion($m[1]); $response = $response->withStatus((int) $m[2], \trim($m[3])); $response = (yield from $this->parseHeaders($stream, $response)); if ($dropBody || Http::isResponseWithoutBody($response->getStatusCode())) { $body = new StringBody(); if (!$dropBody) { $response = $response->withHeader('Content-Length', '0'); } } else { $body = Body::fromMessage($stream, $response); } static $remove = ['TE', 'Trailer']; foreach ($remove as $name) { $response = $response->withoutHeader($name); } return $response->withBody($body); }
protected function readNextChunk() : \Generator { if ($this->expectContinue) { $expect = $this->expectContinue; $this->expectContinue = null; $this->continued = true; (yield $expect->write(Http::getStatusLine(Http::CONTINUE) . "\r\n")); } return (yield $this->stream->read($this->bufferSize)); }
public function __debugInfo() : array { $headers = []; foreach ($this->getHeaders() as $k => $header) { foreach ($header as $v) { $headers[] = \sprintf('%s: %s', Http::normalizeHeaderName($k), $v); } } \sort($headers, SORT_NATURAL); return ['protocol' => \sprintf('HTTP/%s', $this->protocolVersion), 'method' => $this->method, 'uri' => (string) $this->uri, 'target' => $this->getRequestTarget(), 'headers' => $headers, 'addresses' => $this->addresses, 'body' => $this->body, 'attributes' => \array_keys($this->attributes)]; }
public function createUnsupportedMediaTypeResponse() : HttpResponse { $response = new HttpResponse(Http::UNSUPPORTED_MEDIA_TYPE); $accepted = []; foreach ($this->matches as $match) { foreach ($match->handler->getConsumedMediaTypes() as $type) { $accepted[(string) $type] = true; } } if ($accepted) { \ksort($accepted); $response = $response->withHeader('Content-Type', 'application/json;charset="utf-8"'); $response = $response->withBody(new StringBody(\json_encode(['status' => Http::UNSUPPORTED_MEDIA_TYPE, 'reason' => Http::getReason(Http::UNSUPPORTED_MEDIA_TYPE), 'acceptable' => \array_keys($accepted)], \JSON_UNESCAPED_SLASHES | \JSON_HEX_AMP | \JSON_HEX_APOS | \JSON_HEX_QUOT | \JSON_HEX_TAG))); } return $response; }
/** * Serialize HTTP headers to be sent in an FCGI record. * * @param HttpResponse $response * @param int $size * @return string */ protected function serializeHeaders(HttpResponse $response, int $size = null) : string { $reason = \trim($response->getReasonPhrase()); if ('' === $reason) { $reason = Http::getReason($response->getStatusCode()); } $buffer = \sprintf("Status: %03u%s\r\n", $response->getStatusCode(), \rtrim(' ' . $reason)); if ($size !== null) { $buffer .= "Content-Length: {$size}\r\n"; } foreach ($response->getHeaders() as $name => $header) { $name = Http::normalizeHeaderName($name); foreach ($header as $value) { $buffer .= $name . ': ' . $value . "\r\n"; } } return $buffer; }
/** * {@inheritdoc} */ public function upgradeConnection(HttpDriverContext $context, SocketStream $socket, HttpRequest $request, callable $action) : \Generator { if ($this->isPrefaceRequest($request)) { return yield from $this->upgradeConnectionDirect($context, $socket, $request, $action); } $settings = @\base64_decode($request->getHeaderLine('HTTP2-Settings')); if ($settings === false) { throw new StatusException(Http::CODE_BAD_REQUEST, 'HTTP/2 settings are not properly encoded'); } // Discard request body before switching to HTTP/2. (yield $request->getBody()->discard()); 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' => '-']); } $buffer = Http::getStatusLine(Http::SWITCHING_PROTOCOLS, $request->getProtocolVersion()) . "\r\n"; $buffer .= "Connection: upgrade\r\n"; $buffer .= "Upgrade: h2c\r\n"; (yield $socket->write($buffer . "\r\n")); $conn = new Connection($socket, new HPack($this->hpackContext), $this->logger); (yield $conn->performServerHandshake(new Frame(Frame::SETTINGS, $settings))); 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]); } } } }
/** * Invoke next HTTP middleware or the decorated action. * * @param HttpRequest $request * @return HttpResponse * * @throws \RuntimeException When the middleware / action does not return / resolve into an HTTP response. */ public function __invoke(HttpRequest $request) : \Generator { try { if (isset($this->middlewares[$this->index])) { $response = ($this->middlewares[$this->index++]->callback)($request, $this); } else { $response = ($this->target)($request); } if ($response instanceof \Generator) { $response = (yield from $response); } if (!$response instanceof HttpResponse) { throw new \RuntimeException(\sprintf('Middleware must return an HTTP response, given %s', \is_object($response) ? \get_class($response) : \gettype($response))); } } catch (\Throwable $e) { $response = Http::respondToError($e, $this->logger); } return $response; }
protected function serializeHeaders(HttpRequest $request, int $size = null) { if (\in_array('upgrade', $request->getHeaderTokenValues('Connection'))) { $request = $request->withHeader('Connection', 'upgrade'); } else { $request = $request->withHeader('Connection', $this->keepAlive ? 'keep-alive' : 'close'); } $buffer = \sprintf("%s %s HTTP/%s\r\n", $request->getMethod(), $request->getRequestTarget(), $request->getProtocolVersion()); if ($this->keepAlive) { $buffer .= \sprintf("Keep-Alive: timeout=%u\r\n", $this->pool->getMaxLifetime()); } if ($size === null) { $buffer .= "Transfer-Encoding: chunked\r\n"; } else { $buffer .= "Content-Length: {$size}\r\n"; } foreach ($request->getHeaders() as $name => $header) { $name = Http::normalizeHeaderName($name); foreach ($header as $value) { $buffer .= $name . ': ' . $value . "\r\n"; } } return $buffer; }
/** * Serialize HTTP response headers into a string. */ protected function serializeHeaders(HttpResponse $response, bool &$close, int $size = null, bool $nobody = false, bool $deferred = false) : string { $reason = \trim($response->getReasonPhrase()); if ($reason === '') { $reason = \trim(Http::getReason($response->getStatusCode())); } if (!$response->hasHeader('Connection')) { $response = $response->withHeader('Connection', $close ? 'close' : 'keep-alive'); } if (!$close) { $response = $response->withHeader('Keep-Alive', '30'); } $buffer = \sprintf("HTTP/%s %u%s\r\n", $response->getProtocolVersion(), $response->getStatusCode(), \rtrim(' ' . $reason)); if (!$nobody) { if ($deferred) { $close = true; } else { if ((double) $response->getProtocolVersion() > 1) { if ($size === null) { $buffer .= "Transfer-Encoding: chunked\r\n"; } else { $buffer .= "Content-Length: {$size}\r\n"; } } elseif ($size !== null) { $buffer .= "Content-Length: {$size}\r\n"; } else { $close = true; } } } foreach ($response->getHeaders() as $name => $header) { $name = Http::normalizeHeaderName($name); foreach ($header as $value) { $buffer .= $name . ': ' . $value . "\r\n"; } } return $buffer; }
/** * Check if the given response payload should be compressed. * * @param HttpRequest $request * @param HttpResponse $response * @return bool */ protected function isCompressable(HttpRequest $request, HttpResponse $response) : bool { if ($request->getMethod() === Http::HEAD) { return false; } if ($response->getBody() instanceof DeferredBody || Http::isResponseWithoutBody($response->getStatusCode())) { return false; } if ($response->hasHeader('Content-Encoding') || !$response->hasHeader('Content-Type')) { return false; } try { $media = $response->getContentType()->getMediaType(); } catch (InvalidMediaTypeException $e) { return false; } if (isset($this->types[(string) $media])) { return true; } foreach ($media->getSubTypes() as $sub) { if (isset($this->subTypes[$sub])) { return true; } } return false; }