diff --git a/src/Connection/Http2Connection.php b/src/Connection/Http2Connection.php index 2c3727c..70aad5c 100644 --- a/src/Connection/Http2Connection.php +++ b/src/Connection/Http2Connection.php @@ -10,7 +10,9 @@ use NoccyLabs\React\Http2\Frame\HeadersFrame; use NoccyLabs\React\Http2\Frame\SettingsFrame; use NoccyLabs\React\Http2\Stream\Http2Stream; use Psr\Http\Message\ServerRequestInterface; +use React\Stream\CompositeStream; use React\Stream\DuplexStreamInterface; +use React\Stream\ThroughStream; /** * HTTP/2 Connecton Classs @@ -23,10 +25,11 @@ class Http2Connection implements EventEmitterInterface /** @var string Emitted when a new stream is opened */ const EVENT_OPEN = 'open'; - /** @var string Emitted when a the connection (and effectively all its streams) are closed */ const EVENT_CLOSE = 'close'; + const EVENT_ERROR = 'error'; + /** @var array The active streams, even for s2c, odd for c2s */ private array $streams = []; @@ -36,16 +39,29 @@ class Http2Connection implements EventEmitterInterface /** @var int Max number of streams that can be opened; assigned from settings frame */ private int $maxStream = 0; + private ServerRequestInterface $request; + /** @var DuplexStreamInterface The connection to the client */ private DuplexStreamInterface $connection; + private DuplexStreamInterface $responseStream; + + private ?SettingsFrame $settings = null; + private string $buffer = ''; - public function __construct(DuplexStreamInterface $connection, ServerRequestInterface $request, ?SettingsFrame $http2settings) + public function __construct(ServerRequestInterface $request, ?SettingsFrame $http2Settings) { - $this->connection = $connection; + $this->request = $request; + $this->settings = $http2Settings; - $connection->on('data', function ($data) { + $responseInputStream = new ThroughStream(); + $responseOutputStream = new ThroughStream(); + + $this->responseStream = new CompositeStream($responseOutputStream, $responseInputStream); + $this->connection = new CompositeStream($responseInputStream, $responseOutputStream); + + $this->connection->on('data', function ($data) { $this->buffer .= $data; $frame = Frame::parseFrame($this->buffer); $this->handleHttp2Frame($frame); @@ -85,7 +101,15 @@ class Http2Connection implements EventEmitterInterface } - + /** + * Create a pair of streams, one for incoming and one for outgoing data. + * + * @return Http2Stream[] + */ + private function createStreamPair(): array + { + + } /** * When a new stream is opened, clean up older streams that are idle or closed. @@ -96,4 +120,9 @@ class Http2Connection implements EventEmitterInterface { } + + public function getResponseStream(): DuplexStreamInterface + { + return $this->responseStream; + } } \ No newline at end of file diff --git a/src/Http2Middleware.php b/src/Http2Middleware.php index e68ca9f..647a6f8 100644 --- a/src/Http2Middleware.php +++ b/src/Http2Middleware.php @@ -22,16 +22,20 @@ class Http2Middleware /** @var SplObjectStorage Active connections */ private SplObjectStorage $connections; - public function __construct() + 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)), @@ -45,28 +49,19 @@ class Http2Middleware return Response::plaintext("Unsupported protocol")->withStatus(Response::STATUS_BAD_REQUEST); } - // handle HTTP/2 upgrade from HTTP/1.1 - $http2SettingsData = $request->getHeaderLine("http2-settings"); - if ($http2SettingsData) { - $http2Settings = $this->parseSettingsFromBase64String($http2SettingsData); - } else { - // TODO handle HTTP/2 with prior knowledge - return Response::plaintext("Expected HTTP2-Settings header")->withStatus(Response::STATUS_BAD_REQUEST); + try { + $connection = $this->setupConnection($request); + } + catch (\Exception $e) { + return Response::plaintext("Error upgrading connection")->withStatus(Response::STATUS_BAD_REQUEST); } - - $responseInputStream = new ThroughStream(); - $responseOutputStream = new ThroughStream(); - $responseStream = new CompositeStream($responseOutputStream, $responseInputStream); - $serverStream = new CompositeStream($responseInputStream, $responseOutputStream); - - $connection = new Http2Connection($serverStream, $request, $http2Settings??null); $headers = [ 'Connection' => 'Upgrade', 'Upgrade' => $requestUpgrade ]; - return (new Response(Response::STATUS_SWITCHING_PROTOCOLS, $headers, $responseStream)); + return (new Response(Response::STATUS_SWITCHING_PROTOCOLS, $headers, $connection->getResponseStream())); } @@ -93,8 +88,19 @@ class Http2Middleware */ private function setupConnection(ServerRequestInterface $request): Http2Connection { - $stream = new ThroughStream(); - $connection = new Http2Connection($stream); + // 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) { diff --git a/tests/Connection/Http2ConnectionTest.php b/tests/Connection/Http2ConnectionTest.php new file mode 100644 index 0000000..ee22a72 --- /dev/null +++ b/tests/Connection/Http2ConnectionTest.php @@ -0,0 +1,11 @@ + "h2", "x-forwarded-proto" => "http" ]); - $middleware = new Http2Middleware(); + /** @var ResponseInterface $response */ + $response = $middleware($request); + + $this->assertEquals(Response::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertEquals("Unsupported protocol", $response->getBody()); + + $request = new ServerRequest("GET", "/", [ + "Upgrade" => "h2c", + "x-forwarded-proto" => "http", + ]); /** @var ResponseInterface $response */ $response = $middleware($request); $this->assertEquals(Response::STATUS_BAD_REQUEST, $response->getStatusCode()); + $this->assertEquals("Unsupported protocol", $response->getBody()); + } public function testHandlingUpgradeRequest()