Added tests for Http2Middleware

* Added unit tests for middleware
* Message handling code
This commit is contained in:
Chris 2024-07-27 13:23:18 +02:00
parent d41cfb1be2
commit dc3225538f
4 changed files with 152 additions and 6 deletions

View File

@ -4,7 +4,12 @@ namespace NoccyLabs\React\Http2\Connection;
use Evenement\EventEmitterInterface; use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait; 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 NoccyLabs\React\Http2\Stream\Http2Stream;
use Psr\Http\Message\ServerRequestInterface;
use React\Stream\DuplexStreamInterface; use React\Stream\DuplexStreamInterface;
/** /**
@ -34,13 +39,54 @@ class Http2Connection implements EventEmitterInterface
/** @var DuplexStreamInterface The connection to the client */ /** @var DuplexStreamInterface The connection to the client */
private DuplexStreamInterface $connection; private DuplexStreamInterface $connection;
public function __construct(DuplexStreamInterface $connection) private string $buffer = '';
public function __construct(DuplexStreamInterface $connection, ServerRequestInterface $request, ?SettingsFrame $http2settings)
{ {
$this->connection = $connection; $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. * When a new stream is opened, clean up older streams that are idle or closed.
* *

View File

@ -3,8 +3,15 @@
namespace NoccyLabs\React\Http2; namespace NoccyLabs\React\Http2;
use NoccyLabs\React\Http2\Connection\Http2Connection; 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\Frame\SettingsFrame;
use NoccyLabs\React\Http2\Header\HeaderPacker;
use Psr\Http\Message\ServerRequestInterface; 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\DuplexResourceStream;
use React\Stream\DuplexStreamInterface; use React\Stream\DuplexStreamInterface;
use React\Stream\ThroughStream; use React\Stream\ThroughStream;
@ -24,11 +31,46 @@ class Http2Middleware
{ {
// expect upgrade h2 for secure connections, h2c for plaintext // expect upgrade h2 for secure connections, h2c for plaintext
// TODO handle HTTP/2 upgrade from HTTP/1.1 $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 // 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. * Parse the settings frame present in the HTTP/1.1 upgrade request.
* *

View File

@ -10,6 +10,7 @@ use NoccyLabs\React\Http2\Connection\Http2Connection;
* *
* *
*/ */
// TODO add DuplexStreamInterface here
class Http2Stream class Http2Stream
{ {
const STATE_IDLE = 0; const STATE_IDLE = 0;
@ -22,6 +23,13 @@ class Http2Stream
private int $state = self::STATE_IDLE; private int $state = self::STATE_IDLE;
private int $index;
private Http2Connection $connection; private Http2Connection $connection;
public function __construct(Http2Connection $connection, int $index)
{
}
} }

View File

@ -0,0 +1,50 @@
<?php
namespace NoccyLabs\React\Http2;
use NoccyLabs\React\Http2\Frame\SettingsFrame;
use PHPUnit\Framework\Attributes\CoversClass;
use Psr\Http\Message\ResponseInterface;
use React\Http\Message\Response;
use React\Http\Message\ServerRequest;
#[CoversClass(Http2Middleware::class)]
class Http2MiddlewareTest extends \PHPUnit\Framework\TestCase
{
public function testInvalidUpgradeRequests()
{
$request = new ServerRequest("GET", "/", [
"Upgrade" => "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());
}
}