From dc3225538f96f3dd1cbed44d26df5d52d8bbdd10 Mon Sep 17 00:00:00 2001 From: Christopher Vagnetoft Date: Sat, 27 Jul 2024 13:23:18 +0200 Subject: [PATCH] Added tests for Http2Middleware * Added unit tests for middleware * Message handling code --- src/Connection/Http2Connection.php | 50 ++++++++++++++++++++++++++++-- src/Http2Middleware.php | 48 ++++++++++++++++++++++++++-- src/Stream/Http2Stream.php | 10 +++++- tests/Http2MiddlewareTest.php | 50 ++++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 tests/Http2MiddlewareTest.php diff --git a/src/Connection/Http2Connection.php b/src/Connection/Http2Connection.php index f8ec46e..2c3727c 100644 --- a/src/Connection/Http2Connection.php +++ b/src/Connection/Http2Connection.php @@ -4,7 +4,12 @@ namespace NoccyLabs\React\Http2\Connection; use Evenement\EventEmitterInterface; use Evenement\EventEmitterTrait; +use NoccyLabs\React\Http2\Frame\DataFrame; +use NoccyLabs\React\Http2\Frame\Frame; +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\DuplexStreamInterface; /** @@ -34,13 +39,54 @@ class Http2Connection implements EventEmitterInterface /** @var DuplexStreamInterface The connection to the client */ private DuplexStreamInterface $connection; - public function __construct(DuplexStreamInterface $connection) + private string $buffer = ''; + + public function __construct(DuplexStreamInterface $connection, ServerRequestInterface $request, ?SettingsFrame $http2settings) { $this->connection = $connection; - + $connection->on('data', function ($data) { + $this->buffer .= $data; + $frame = Frame::parseFrame($this->buffer); + $this->handleHttp2Frame($frame); + }); + } + private function handleHttp2Frame(Frame $frame) + { + switch ($frame->getFrameType()) { + case Frame::FRAME_HEADERS: + $this->handleHttp2HeadersFrame($frame); + break; + case Frame::FRAME_DATA: + $this->handleHttp2DataFrame($frame); + break; + case Frame::FRAME_PING: + $this->handleHttp2PingFrame($frame); + break; + } + } + + private function handleHttp2HeadersFrame(HeadersFrame $frame) + { + $method = $frame->headers->getHeaderLine(':method'); + $scheme = $frame->headers->getHeaderLine(':scheme'); + $path = $frame->headers->getHeaderLine(':path'); + } + + private function handleHttp2DataFrame(DataFrame $frame) + { + + } + + private function handleHttp2PingFrame(DataFrame $frame) + { + + } + + + /** * When a new stream is opened, clean up older streams that are idle or closed. * diff --git a/src/Http2Middleware.php b/src/Http2Middleware.php index d26b406..e68ca9f 100644 --- a/src/Http2Middleware.php +++ b/src/Http2Middleware.php @@ -3,8 +3,15 @@ namespace NoccyLabs\React\Http2; use NoccyLabs\React\Http2\Connection\Http2Connection; +use NoccyLabs\React\Http2\Frame\DataFrame; +use NoccyLabs\React\Http2\Frame\Frame; +use NoccyLabs\React\Http2\Frame\HeadersFrame; use NoccyLabs\React\Http2\Frame\SettingsFrame; +use NoccyLabs\React\Http2\Header\HeaderPacker; use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use React\Http\Message\Response; +use React\Stream\CompositeStream; use React\Stream\DuplexResourceStream; use React\Stream\DuplexStreamInterface; use React\Stream\ThroughStream; @@ -24,11 +31,46 @@ class Http2Middleware { // expect upgrade h2 for secure connections, h2c for plaintext - // TODO handle HTTP/2 upgrade from HTTP/1.1 - // TODO handle HTTP/2 with prior knowledge + $requestSecure = $request->getHeaderLine("x-forwarded-proto") ?: "http"; + $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); + } + + // 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); + } + + $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)); } + /** * Parse the settings frame present in the HTTP/1.1 upgrade request. * @@ -61,4 +103,4 @@ class Http2Middleware return $connection; } -} \ No newline at end of file +} diff --git a/src/Stream/Http2Stream.php b/src/Stream/Http2Stream.php index 0ec96fd..b075669 100644 --- a/src/Stream/Http2Stream.php +++ b/src/Stream/Http2Stream.php @@ -10,6 +10,7 @@ use NoccyLabs\React\Http2\Connection\Http2Connection; * * */ +// TODO add DuplexStreamInterface here class Http2Stream { const STATE_IDLE = 0; @@ -22,6 +23,13 @@ class Http2Stream private int $state = self::STATE_IDLE; + private int $index; + private Http2Connection $connection; -} \ No newline at end of file + public function __construct(Http2Connection $connection, int $index) + { + + } + +} diff --git a/tests/Http2MiddlewareTest.php b/tests/Http2MiddlewareTest.php new file mode 100644 index 0000000..4be6f6a --- /dev/null +++ b/tests/Http2MiddlewareTest.php @@ -0,0 +1,50 @@ + "h2", + "x-forwarded-proto" => "http" + ]); + + $middleware = new Http2Middleware(); + + /** @var ResponseInterface $response */ + $response = $middleware($request); + + $this->assertEquals(Response::STATUS_BAD_REQUEST, $response->getStatusCode()); + } + + public function testHandlingUpgradeRequest() + { + $http2settings = new SettingsFrame(); + $http2settings->set(SettingsFrame::SETTINGS_MAX_CONCURRENT_STREAMS, 64); + + $request = new ServerRequest("GET", "/", [ + "Upgrade" => "h2c", + "Connection" => "upgrade", + "x-forwarded-proto" => "http", + "HTTP2-Settings" => $http2settings->toBinary(), + ]); + + $middleware = new Http2Middleware(); + + /** @var ResponseInterface $response */ + $response = $middleware($request); + + $this->assertEquals(Response::STATUS_SWITCHING_PROTOCOLS, $response->getStatusCode()); + } + +}