react-http2/src/Http2Middleware.php
Christopher Vagnetoft dc3225538f Added tests for Http2Middleware
* Added unit tests for middleware
* Message handling code
2024-07-27 13:23:18 +02:00

107 lines
3.6 KiB
PHP

<?php
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;
use SplObjectStorage;
class Http2Middleware
{
/** @var SplObjectStorage<Http2Connection> Active connections */
private SplObjectStorage $connections;
public function __construct()
{
$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";
$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.
*
* @param string $settings
* @return SettingsFrame
*/
private function parseSettingsFromBase64String(string $settings): SettingsFrame
{
$decoded = base64_decode($settings);
$frame = new SettingsFrame();
$frame->fromBinary($decoded);
return $frame;
}
/**
* Prepare a connection for the HTTP/2 session.
*
* @param ServerRequestInterface $request
* @return Http2Connection
*/
private function setupConnection(ServerRequestInterface $request): Http2Connection
{
$stream = new ThroughStream();
$connection = new Http2Connection($stream);
$this->connections->attach($connection);
$connection->on('close', function () use ($connection) {
$this->connections->detach($connection);
});
return $connection;
}
}