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); }
/** * Check if the HTTP request matches a public file and server it as needed. * * @param HttpRequest $request * @param NextMiddleware $next * @return HttpResponse */ public function __invoke(HttpRequest $request, NextMiddleware $next) : \Generator { static $methods = [Http::HEAD, Http::GET]; if (!\in_array($request->getMethod(), $methods, true)) { return yield from $next($request); } $path = '/' . \trim($request->getRequestTarget(), '/'); if ($this->basePath !== '/') { if (0 !== \strpos($path, $this->basePath)) { return yield from $next($request); } $path = \substr($path, \strlen($this->basePath) - 1); } $file = Filesystem::normalizePath($this->directory . \substr($path, 1)); if (0 !== \strpos($file, $this->directory)) { return yield from $next($request); } if (!(yield LoopConfig::currentFilesystem()->isFile($file))) { return yield from $next($request); } return $this->createResponse($request, $file); }
/** * {@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; }
/** * Assert that an upgrade to WebSockets is possible. * * @param HttpRequest $request * @param HttpResponse $response * @return HttpResponse when upgrading is impossible, NULL if everything's fine. */ protected function assertUpgradePossible(HttpRequest $request, HttpResponse $response) { if ($request->getMethod() !== Http::METHOD_GET) { return $response->withStatus(Http::CODE_METHOD_NOT_ALLOWED)->withHeader('Allow', 'GET'); } if ($request->getProtocolVersion() !== '1.1') { return $response->withStatus(Http::CODE_HTTP_VERSION_NOT_SUPPORTED); } if (!$request->hasHeader('Sec-WebSocket-Key')) { return $response->withStatus(Http::CODE_BAD_REQUEST, 'Missing Sec-Websocket-Key header'); } if ($request->hasHeader('Sec-Websocket-Version') && !in_array('13', $request->getHeader('Sec-Websocket-Version'), true)) { return $response->withStatus(Http::CODE_BAD_REQUEST, 'Web socket version 13 required')->withHeader('Sec-WebSocket-Version', '13'); } }
/** * 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']); } }
/** * 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; }
protected function sendDeferredResponse(SocketStream $socket, HttpRequest $request, HttpResponse $response, bool $nobody, bool $close) : \Generator { 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) { $close = true; } (yield $socket->write($this->serializeHeaders($response, $close, null, $nobody, true) . "\r\n")); (yield $socket->flush()); if ($nobody) { $response->getBody()->close(false); return !$close; } $watcher = null; $task = new Coroutine(function () use(&$watcher, $socket, $request, $response) { $body = $response->getBody(); $body->start($request, $this->logger); $bodyStream = (yield $body->getReadableStream()); $e = null; try { while (null !== ($chunk = (yield $bodyStream->read()))) { (yield $socket->write($chunk)); } } catch (StreamClosedException $e) { // Client disconnected from server. } finally { try { $bodyStream->close(); } finally { $body->close($e ? true : false); } $watcher->cancel(new \RuntimeException()); } return false; }, true); $watcher = new Coroutine(function () use($socket, $task) { $socket = $socket->getSocket(); while (\is_resource($socket) && !\feof($socket)) { (yield new AwaitRead($socket)); } $task->cancel(new StreamClosedException('Client disconnected')); }); return (yield $task); }
/** * 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; }