Cleanup middleware logic

* Http2Connection now sets up the streams and returns a responseStream
This commit is contained in:
Chris 2024-07-27 16:21:24 +02:00
parent dc3225538f
commit 0699123f5e
4 changed files with 84 additions and 25 deletions

View File

@ -10,7 +10,9 @@ use NoccyLabs\React\Http2\Frame\HeadersFrame;
use NoccyLabs\React\Http2\Frame\SettingsFrame; use NoccyLabs\React\Http2\Frame\SettingsFrame;
use NoccyLabs\React\Http2\Stream\Http2Stream; use NoccyLabs\React\Http2\Stream\Http2Stream;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use React\Stream\CompositeStream;
use React\Stream\DuplexStreamInterface; use React\Stream\DuplexStreamInterface;
use React\Stream\ThroughStream;
/** /**
* HTTP/2 Connecton Classs * HTTP/2 Connecton Classs
@ -23,10 +25,11 @@ class Http2Connection implements EventEmitterInterface
/** @var string Emitted when a new stream is opened */ /** @var string Emitted when a new stream is opened */
const EVENT_OPEN = 'open'; const EVENT_OPEN = 'open';
/** @var string Emitted when a the connection (and effectively all its streams) are closed */ /** @var string Emitted when a the connection (and effectively all its streams) are closed */
const EVENT_CLOSE = 'close'; const EVENT_CLOSE = 'close';
const EVENT_ERROR = 'error';
/** @var array<int,Http2Stream> The active streams, even for s2c, odd for c2s */ /** @var array<int,Http2Stream> The active streams, even for s2c, odd for c2s */
private array $streams = []; 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 */ /** @var int Max number of streams that can be opened; assigned from settings frame */
private int $maxStream = 0; private int $maxStream = 0;
private ServerRequestInterface $request;
/** @var DuplexStreamInterface The connection to the client */ /** @var DuplexStreamInterface The connection to the client */
private DuplexStreamInterface $connection; private DuplexStreamInterface $connection;
private DuplexStreamInterface $responseStream;
private ?SettingsFrame $settings = null;
private string $buffer = ''; 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; $this->buffer .= $data;
$frame = Frame::parseFrame($this->buffer); $frame = Frame::parseFrame($this->buffer);
$this->handleHttp2Frame($frame); $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. * 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;
}
} }

View File

@ -22,16 +22,20 @@ class Http2Middleware
/** @var SplObjectStorage<Http2Connection> Active connections */ /** @var SplObjectStorage<Http2Connection> Active connections */
private SplObjectStorage $connections; private SplObjectStorage $connections;
public function __construct() private array $handlers;
public function __construct(callable ...$httpHandlers)
{ {
$this->handlers = $httpHandlers;
$this->connections = new SplObjectStorage(); $this->connections = new SplObjectStorage();
} }
public function __invoke(ServerRequestInterface $request, ?callable $next=null) public function __invoke(ServerRequestInterface $request, ?callable $next=null)
{ {
// expect upgrade h2 for secure connections, h2c for plaintext // expect upgrade h2 for secure connections, h2c for plaintext
$requestSecure = $request->getHeaderLine("x-forwarded-proto") ?: "http"; $requestSecure = $request->getHeaderLine("x-forwarded-proto") ?: "http";
// Parse out headers
$requestUpgrade = $request->getHeaderLine("upgrade"); $requestUpgrade = $request->getHeaderLine("upgrade");
$connectionFlags = array_map( $connectionFlags = array_map(
fn($v) => strtolower(trim($v)), fn($v) => strtolower(trim($v)),
@ -45,28 +49,19 @@ class Http2Middleware
return Response::plaintext("Unsupported protocol")->withStatus(Response::STATUS_BAD_REQUEST); return Response::plaintext("Unsupported protocol")->withStatus(Response::STATUS_BAD_REQUEST);
} }
// handle HTTP/2 upgrade from HTTP/1.1 try {
$http2SettingsData = $request->getHeaderLine("http2-settings"); $connection = $this->setupConnection($request);
if ($http2SettingsData) { }
$http2Settings = $this->parseSettingsFromBase64String($http2SettingsData); catch (\Exception $e) {
} else { return Response::plaintext("Error upgrading connection")->withStatus(Response::STATUS_BAD_REQUEST);
// 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 = [ $headers = [
'Connection' => 'Upgrade', 'Connection' => 'Upgrade',
'Upgrade' => $requestUpgrade '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 private function setupConnection(ServerRequestInterface $request): Http2Connection
{ {
$stream = new ThroughStream(); // handle HTTP/2 upgrade from HTTP/1.1
$connection = new Http2Connection($stream); $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); $this->connections->attach($connection);
$connection->on('close', function () use ($connection) { $connection->on('close', function () use ($connection) {

View File

@ -0,0 +1,11 @@
<?php
namespace NoccyLabs\React\Http2\Connection;
use PHPUnit\Framework\Attributes\CoversClass;
#[CoversClass(Http2Connection::class)]
class Http2ConnectionTest extends \PHPUnit\Framework\TestCase
{
}

View File

@ -14,17 +14,30 @@ class Http2MiddlewareTest extends \PHPUnit\Framework\TestCase
public function testInvalidUpgradeRequests() public function testInvalidUpgradeRequests()
{ {
$middleware = new Http2Middleware();
$request = new ServerRequest("GET", "/", [ $request = new ServerRequest("GET", "/", [
"Upgrade" => "h2", "Upgrade" => "h2",
"x-forwarded-proto" => "http" "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 */ /** @var ResponseInterface $response */
$response = $middleware($request); $response = $middleware($request);
$this->assertEquals(Response::STATUS_BAD_REQUEST, $response->getStatusCode()); $this->assertEquals(Response::STATUS_BAD_REQUEST, $response->getStatusCode());
$this->assertEquals("Unsupported protocol", $response->getBody());
} }
public function testHandlingUpgradeRequest() public function testHandlingUpgradeRequest()