/** * {@inheritdoc} */ public function readBody(HttpRequest $request, \ReflectionClass $type) : \Generator { $input = (yield $request->getBody()->getContents()); $xml = new \DOMDocument(); $xml->formatOutput = false; \libxml_clear_errors(); $errorHandling = \libxml_use_internal_errors(true); $entities = \libxml_disable_entity_loader(true); try { $success = @$xml->loadXML($input, \LIBXML_NONET | \LIBXML_NOENT); $errors = \libxml_get_errors(); } catch (\Throwable $e) { if (!empty($errors) && $this->logger) { $this->logErrors($errors); } throw new StatusException(Http::BAD_REQUEST, 'Invalid XML input', [], $e); } finally { \libxml_use_internal_errors($errorHandling); \libxml_disable_entity_loader($entities); } if (!empty($errors) || empty($success) || $xml === NULL || !$xml instanceof \DOMDocument) { if (!empty($errors) && $this->logger) { $this->logErrors($errors); } throw new StatusException(Http::BAD_REQUEST, 'Invalid XML input'); } return $xml; }
/** * {@inheritdoc} */ public function readBody(HttpRequest $request, \ReflectionClass $type) : \Generator { $data = @\json_decode((yield $request->getBody()->getContents()), true); if ($data === null && \json_last_error() !== \JSON_ERROR_NONE) { throw new StatusException(Http::BAD_REQUEST, \sprintf('Malformed JSON data received: "%s"', \json_last_error_msg())); } return new JsonBody($data); }
/** * {@inheritdoc} */ public function start(HttpRequest $request, LoggerInterface $logger = null) { $this->logger = $logger; if ($this->logger) { $this->address = $request->getClientAddress(); $this->logger->debug('Enabled SSE for {address} using HTTP/{version}', ['address' => $this->address, 'version' => $request->getProtocolVersion()]); } }
/** * {@inheritdoc} */ public function send(HttpRequest $request) : Awaitable { if (!$request->hasHeader('User-Agent')) { $request = $request->withHeader('User-Agent', $this->userAgent); } $invoke = \Closure::bind(function (HttpRequest $request) { $next = new NextMiddleware($this->middlewares, function (HttpRequest $request) { return \array_shift($this->responses); }); return new Coroutine($next($request)); }, $this, HttpClient::class); return $invoke($request); }
/** * {@inheritdoc} */ public function __invoke(HttpRequest $request, NextMiddleware $next) : \Generator { static $methods = [Http::HEAD => true, Http::GET => true]; if (empty($methods[$request->getMethod()])) { return yield from $next($request); } $path = '/' . \trim(Uri::decode($request->getRequestTarget()), '/'); if (0 !== \strpos($path, $this->base)) { return yield from $next($request); } $path = \substr($path, \strlen($this->base)); try { $file = $this->locator->locatePublicFile($path); } catch (ResourceNotFoundException $e) { return yield from $next($request); } $ext = false === ($pos = \strpos($file, '.')) ? '' : \strtolower(\substr($file, $pos + 1)); foreach ($this->processors as $processor) { $result = $processor($request, $file, $ext); if ($result instanceof \Generator) { $result = (yield from $result); } if ($result instanceof HttpResponse) { $response = $result; break; } } if (!isset($response)) { $response = new FileResponse($file); } if (!$response->hasHeader('Cache-Control')) { $response = $response->withHeader('Cache-Control', \sprintf('public, max-age=%u', $this->ttl)); if ($request->getProtocolVersion() === '1.0') { $response = $response->withHeader('Expires', \gmdate(Http::DATE_RFC1123, \time() + $this->ttl)); } } return $response; }
/** * Automatically follow HTTP redirects according to HTTP status code and location header. * * Uncached HTTP request bodies will be cached prior to being sent to the remote endpoint. * * @param HttpRequest $request * @param NextMiddleware $next * @return HttpResponse * * @throws TooManyRedirectsException When the maximum number of redirects for a single HTTP request has been exceeded. */ public function __invoke(HttpRequest $request, NextMiddleware $next) : \Generator { $body = $request->getBody(); if (!$body->isCached()) { $request = $request->withBody(new BufferedBody((yield $body->getReadableStream()))); } for ($i = -1; $i < $this->maxRedirects; $i++) { $response = (yield from $next($request)); switch ($response->getStatusCode()) { case Http::MOVED_PERMANENTLY: case Http::FOUND: case Http::SEE_OTHER: $request = $request->withMethod(Http::GET); $request = $request->withoutHeader('Content-Type'); $request = $request->withBody(new StringBody()); break; case Http::TEMPORARY_REDIRECT: case Http::PERMANENT_REDIRECT: // Replay request to a different URL. break; default: return $response; } try { $uri = Uri::parse($response->getHeaderLine('Location')); $target = $uri->getPath(); if ('' !== ($query = $uri->getQuery())) { $target .= '?' . $query; } $request = $request->withUri($uri); $request = $request->withRequestTarget($target); } finally { (yield $response->getBody()->discard()); } } throw new TooManyRedirectsException(\sprintf('Limit of %s HTTP redirects exceeded', $this->maxRedirects)); }
public function __invoke(HttpRequest $request, HttpDriverContext $context) : \Generator { $path = \str_replace('+', '%20', $request->getRequestTarget()); $routing = new RoutingContext($this->container, $this->compiler, $path, $request->getMethod(), $this->routable->getMiddlewares()); $this->routeRequest($routing); if (!$routing->isMatch()) { return $routing->createNoMatchResponse(); } $matches = []; if ($request->hasHeader('Content-Type')) { $type = $request->getContentType()->getMediaType(); foreach ($routing->getMatches() as $match) { if ($match->handler->canConsume($type)) { $matches[] = $match; } } } else { foreach ($routing->getMatches() as $match) { if ($match->handler->isConsumer()) { continue; } $matches[] = $match; } } if (!$matches) { return $routing->createUnsupportedMediaTypeResponse(); } $accept = $request->getAccept(); $result = $matches[0]; foreach ($accept->getMediaTypes() as $media) { foreach ($matches as $match) { foreach ($match->handler->getProducedMediaTypes() as $type) { if ($media->is($type)) { $result = $match; break 3; } } } } if (empty($result->middlewares)) { return yield from $this->dispatchRequest($request, $context, $result); } $next = new NextMiddleware($result->middlewares, function (HttpRequest $request) use($context, $result) { return yield from $this->dispatchRequest($request, $context, $result); }, $this->logger); return yield from $next($request); }
/** * Send handshake HTTP/1.1 response to the client to enable protocol switching. * * @param BufferedDuplexStreamInterface $socket * @param HttpRequest $request */ protected function sendHandshake(BufferedDuplexStreamInterface $socket, HttpRequest $request) : \Generator { $accept = base64_encode(sha1($request->getHeaderLine('Sec-WebSocket-Key') . self::GUID, true)); $message = sprintf("HTTP/%s 101 Switching Protocols\r\n", $request->getProtocolVersion()); $message .= "Connection: Upgrade\r\n"; $message .= "Upgrade: websocket\r\n"; $message .= sprintf("Sec-WebSocket-Accept: %s\r\n", $accept); $message .= "Sec-WebSocket-Version: 13\r\n"; $message .= "\r\n"; yield from $socket->write($message); }
/** * Assert that the given HTTP request can be upgraded to the WebSocket protocol. */ protected function assertUpgradePossible(HttpRequest $request) { if ($request->getMethod() !== Http::GET) { throw new StatusException(Http::METHOD_NOT_ALLOWED, 'WebSocket upgrade requires an HTTP GET request', ['Allow' => Http::GET, 'Sec-Websocket-Version' => '13']); } if (!$request->hasHeader('Sec-Websocket-Key')) { throw new StatusException(Http::BAD_REQUEST, 'Missing Sec-Websocket-Key HTTP header', ['Sec-Websocket-Version' => '13']); } if (!\in_array('13', $request->getHeaderTokenValues('Sec-Websocket-Version'), true)) { throw new StatusException(Http::BAD_REQUEST, 'Secure websocket version 13 required', ['Sec-Websocket-Version' => '13']); } }
/** * Create an HTTP file response for the matched file. * * @param HttpRequest $request * @param string $file * @return HttpResponse */ protected function createResponse(HttpRequest $request, string $file) : HttpResponse { $response = new FileResponse($file); $response = $response->withHeader('Cache-Control', \sprintf('public, max-age=%u', $this->ttl)); if ($request->getProtocolVersion() === '1.0') { $response = $response->withHeader('Expires', \gmdate(Http::DATE_RFC1123, \time() + $this->ttl)); } return $response; }
/** * Convert the given outcome of an action into an HTTP response. * * @param HttpRequest $request * @param mixed $result * @return HttpResponse */ public function respond(HttpRequest $request, $result) : HttpResponse { if ($result instanceof HttpResponse) { return $result->withProtocolVersion($request->getProtocolVersion()); } foreach ($this->responders as $responder) { $response = ($responder->callback)($request, $result); if ($response instanceof HttpResponse) { return $response->withProtocolVersion($request->getProtocolVersion()); } } $reason = \sprintf('Expecting HttpResponse, given %s', \is_object($result) ? \get_class($result) : \gettype($result)); throw new \RuntimeException($reason); }
/** * Normalize the given HTTP response to be sent using FCGI records. * * @param HttpRequest $request * @param HttpResponse $response * @return HttpResponse */ protected function normalizeResponse(HttpRequest $request, HttpResponse $response) : HttpResponse { static $remove = ['Connection', 'Content-Length', 'Keep-Alive', 'Status', 'Trailer', 'Transfer-Encoding', 'Upgrade']; $response = $response->withProtocolVersion($request->getProtocolVersion()); foreach ($remove as $name) { $response = $response->withoutHeader($name); } return $response->withHeader('Date', \gmdate(Http::DATE_RFC1123)); }
/** * Send the given HTTP request and fetch the HTTP response from the server. * * @param HttpRequest $request The request to be sent (body will be closed after it has been sent). * @return HttpResponse The HTTP response as returned by the target server. */ public function send(HttpRequest $request) : Awaitable { if (!$request->hasHeader('User-Agent')) { $request = $request->withHeader('User-Agent', $this->userAgent); } $next = new NextMiddleware($this->middlewares, function (HttpRequest $request) { $connecting = new \SplObjectStorage(); $uri = $request->getUri(); foreach ($this->connectors as $connector) { if (!$connector->isRequestSupported($request)) { continue; } $context = (yield $connector->getConnectorContext($uri)); if ($context->connected) { foreach ($connecting as $conn) { $connecting[$conn]->dispose(); } return (yield $connector->send($context, $request)); } $connecting[$connector] = $context; } try { $socket = (yield $this->connectSocket($request->getUri())); $meta = $socket->getMetadata(); try { $connector = $this->chooseConnector($request, \trim($meta['crypto']['alpn_protocol'] ?? ''), $meta); } catch (\Throwable $e) { $socket->close(); throw $e; } $context = $connecting[$connector]; $context->socket = $socket; $connecting->detach($connector); return (yield $connector->send($context, $request)); } finally { foreach ($connecting as $conn) { $connecting[$conn]->dispose(); } } }, $this->logger); return new Coroutine($next($request)); }
/** * Check for a pre-parsed HTTP/2 connection preface. */ protected function isPrefaceRequest(HttpRequest $request) : bool { if ($request->getMethod() !== 'PRI') { return false; } if ($request->getRequestTarget() !== '*') { return false; } if ($request->getProtocolVersion() !== '2.0') { return false; } return true; }
/** * {@inheritdoc} */ public function send(HttpConnectorContext $context, HttpRequest $request) : Awaitable { if (!$context instanceof ConnectorContext) { throw new \InvalidArgumentException('Invalid connector context passed'); } return new Coroutine(function () use($context, $request) { $uri = $request->getUri(); $key = \sprintf('%s://%s', $uri->getScheme(), $uri->getHostWithPort(true)); if ($context->conn) { $conn = $context->conn; } else { $conn = new Connection($context->socket, new HPack($this->hpackContext)); if ($this->logger) { $conn->setLogger($this->logger); } (yield $conn->performClientHandshake()); $this->connections[$key] = $conn; } $request = $request->withProtocolVersion('2.0'); $request = $request->withHeader('Date', \gmdate(Http::DATE_RFC1123)); $sent = 0; $response = (yield $conn->openStream()->sendRequest($request, $sent)); if ($this->logger) { $this->logger->info('{ip} "{method} {target} HTTP/{protocol}" {status} {size}', ['ip' => $request->getClientAddress(), 'method' => $request->getMethod(), 'target' => $request->getRequestTarget(), 'protocol' => $response->getProtocolVersion(), 'status' => $response->getStatusCode(), 'size' => $sent ?: '-']); } if (isset($this->connecting[$key])) { $context->connected = true; $context->conn = $conn; $context->dispose(); } return $response; }); }
/** * Get all known hop IP addresses as provided by an HTTP proxy. * * @param HttpRequest $request * @return array */ public function getAddresses(HttpRequest $request) : array { $addresses = []; foreach ($this->addressHeaders as $name) { if ($request->hasHeader($name)) { foreach ($request->getHeaderTokenValues($name, false) as $ip) { $addresses[] = $ip; } } } return $addresses; }
/** * 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; }
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; }
/** * Normalize HTTP response object prior to being sent to the client. */ protected function normalizeResponse(HttpRequest $request, HttpResponse $response) : HttpResponse { static $remove = ['Content-Length', 'Keep-Alive', 'Trailer', 'Transfer-Encoding']; $response = $response->withProtocolVersion($request->getProtocolVersion()); foreach ($remove as $name) { $response = $response->withoutHeader($name); } $conn = []; foreach ($response->getHeaderTokenValues('Connection') as $token) { switch ($token) { case 'close': case 'keep-alive': // Ignore these... break; default: $conn[] = $token; } } if (empty($conn)) { $response = $response->withoutHeader('Connection'); } else { $response = $response->withHeader('Connection', \implode(', ', $conn)); } return $response->withHeader('Date', \gmdate(Http::DATE_RFC1123)); }