Cleanup middleware logic
* Http2Connection now sets up the streams and returns a responseStream
This commit is contained in:
parent
dc3225538f
commit
0699123f5e
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
@ -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) {
|
||||||
|
11
tests/Connection/Http2ConnectionTest.php
Normal file
11
tests/Connection/Http2ConnectionTest.php
Normal 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
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
@ -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()
|
||||||
|
Loading…
Reference in New Issue
Block a user