Active connections */ private SplObjectStorage $connections; private array $handlers; public function __construct(callable ...$httpHandlers) { $this->handlers = $httpHandlers; $this->connections = new SplObjectStorage(); } public function __invoke(ServerRequestInterface $request, ?callable $next=null) { // expect upgrade h2 for secure connections, h2c for plaintext $requestSecure = $request->getHeaderLine("x-forwarded-proto") ?: "http"; // Parse out headers $requestUpgrade = $request->getHeaderLine("upgrade"); $connectionFlags = array_map( fn($v) => strtolower(trim($v)), explode(",", $request->getHeaderLine("connection")) ); // Pass everything we aren't interested in on to the next handler if (!in_array('upgrade', $connectionFlags) || !in_array($requestUpgrade, ['h2', 'h2c'])) { if (is_callable($next)) return $next($request); return Response::plaintext("Unsupported protocol")->withStatus(Response::STATUS_BAD_REQUEST); } try { $connection = $this->setupConnection($request); } catch (\Exception $e) { return Response::plaintext("Error upgrading connection")->withStatus(Response::STATUS_BAD_REQUEST); } $headers = [ 'Connection' => 'Upgrade', 'Upgrade' => $requestUpgrade ]; return (new Response(Response::STATUS_SWITCHING_PROTOCOLS, $headers, $connection->getResponseStream())); } /** * Parse the settings frame present in the HTTP/1.1 upgrade request. * * @param string $settings * @return SettingsFrame */ private function parseSettingsFromBase64String(string $settings): SettingsFrame { $decoded = base64_decode($settings); $frame = new SettingsFrame(); $frame->fromBinary($decoded); return $frame; } /** * Prepare a connection for the HTTP/2 session. * * @param ServerRequestInterface $request * @return Http2Connection */ private function setupConnection(ServerRequestInterface $request): Http2Connection { // handle HTTP/2 upgrade from HTTP/1.1 $hasSettingsHeader = str_contains("http2-settings", strtolower($request->getHeaderLine("connection"))); $http2Settings = null; if ($hasSettingsHeader) { $http2SettingsData = $request->getHeaderLine("http2-settings"); if (!$http2SettingsData) { throw new \RuntimeException("Expected HTTP2-Settings header"); } $http2Settings = $this->parseSettingsFromBase64String($http2SettingsData); } $connection = new Http2Connection($request, $http2Settings??null); $this->connections->attach($connection); $connection->on('close', function () use ($connection) { $this->connections->detach($connection); }); return $connection; } }