Beispiel #1
0
 /**
  * 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);
 }
Beispiel #2
0
 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));
 }
Beispiel #3
0
 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)];
 }
Beispiel #4
0
 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;
 }
Beispiel #5
0
 /**
  * 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;
 }
Beispiel #6
0
 /**
  * {@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]);
             }
         }
     }
 }
Beispiel #7
0
 /**
  * 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;
 }
Beispiel #8
0
 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;
 }
Beispiel #9
0
 /**
  * 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;
 }
Beispiel #10
0
 /**
  * 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;
 }