/** * 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); }
/** * Coroutine that sends the given HTTP response to the connected client. */ protected function sendResponse(SocketStream $socket, HttpRequest $request, HttpResponse $response, bool $close) : \Generator { // Discard request body in another coroutine. $request->getBody()->discard(); $response = $this->normalizeResponse($request, $response); $http11 = $response->getProtocolVersion() == '1.1'; $head = $request->getMethod() === Http::HEAD; $nobody = $head || Http::isResponseWithoutBody($response->getStatusCode()); $sendfile = false; $body = $response->getBody(); if ($body instanceof DeferredBody) { return yield from $this->sendDeferredResponse($socket, $request, $response, $nobody, $close); } $size = (yield $body->getSize()); if (!$nobody) { if ($body instanceof FileBody && $socket->isSendfileSupported()) { $sendfile = true; } else { $bodyStream = (yield $body->getReadableStream()); if ($nobody || $size === 0) { $chunk = null; $size = 0; $len = 0; } else { $clen = $size === null ? 4089 : 4096; $chunk = (yield $bodyStream->readBuffer($clen)); $len = \strlen($chunk ?? ''); } if ($chunk === null) { $size = 0; } elseif ($len < $clen) { $size = $len; } } } (yield $socket->write($this->serializeHeaders($response, $close, $size, $nobody) . "\r\n")); (yield $socket->flush()); $sent = 0; try { if (!$nobody) { if ($sendfile) { if ($size) { $sent += (yield LoopConfig::currentFilesystem()->sendfile($body->getFile(), $socket->getSocket(), $size)); } } elseif ($http11 && $size === null) { $sent += (yield $socket->write(\dechex($len) . "\r\n" . $chunk . "\r\n")); if ($len === $clen) { $sent += (yield new CopyBytes($bodyStream, $socket, false, null, 4089, function (string $chunk) { return \dechex(\strlen($chunk)) . "\r\n" . $chunk . "\r\n"; })); } $sent += (yield $socket->write("0\r\n\r\n")); } elseif ($chunk !== null) { $sent += (yield $socket->write($chunk)); if ($len === $clen) { $sent += (yield new CopyBytes($bodyStream, $socket, false, $size === null ? null : $size - $len)); } } (yield $socket->flush()); } } finally { if (isset($bodyStream)) { $bodyStream->close(); } } 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' => $sent ?: '-']); } return !$close; }
/** * Send the given HTTP response using FCGI records. * * @param HttpRequest $request * @param HttpResponse $response */ public function sendResponse(HttpRequest $request, HttpResponse $response) : \Generator { $response = $this->normalizeResponse($request, $response); $body = $response->getBody(); $size = (yield $body->getSize()); $head = $request->getMethod() === Http::HEAD; $nobody = $head || Http::isResponseWithoutBody($response->getStatusCode()); $buffer = $this->serializeHeaders($response, $nobody ? null : $size); (yield $this->conn->sendRecord(new Record(Record::FCGI_VERSION_1, Record::FCGI_STDOUT, $this->id, $buffer . "\r\n"))); if ($body instanceof DeferredBody) { 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) { $body->close(false); } else { $task = new Coroutine($this->sendDeferredResponse($request, $body), true); $this->pending->attach($task); $task->when(function () use($task) { if ($this->pending->contains($task)) { $this->pending->detach($task); } }); (yield $task); } } else { $sent = 0; if (!$nobody) { $bodyStream = (yield $body->getReadableStream()); try { $channel = $bodyStream->channel(4096, $size); while (null !== ($chunk = (yield $channel->receive()))) { $sent += (yield $this->conn->sendRecord(new Record(Record::FCGI_VERSION_1, Record::FCGI_STDOUT, $this->id, $chunk))); } } finally { $bodyStream->close(); } } 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' => $sent ?: '-']); } } (yield $this->conn->sendRecord(new Record(Record::FCGI_VERSION_1, Record::FCGI_STDOUT, $this->id, ''))); (yield $this->conn->sendRecord(new Record(Record::FCGI_VERSION_1, Record::FCGI_STDERR, $this->id, ''))); $this->conn->closeHandler($this->id); }
/** * 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; }