react-http2/src/Http2Middleware.php

113 lines
3.7 KiB
PHP
Raw Normal View History

2024-02-25 00:18:33 +01:00
<?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;
2024-02-25 00:18:33 +01:00
use NoccyLabs\React\Http2\Frame\SettingsFrame;
use NoccyLabs\React\Http2\Header\HeaderPacker;
2024-02-25 00:18:33 +01:00
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use React\Http\Message\Response;
use React\Stream\CompositeStream;
2024-02-25 00:18:33 +01:00
use React\Stream\DuplexResourceStream;
use React\Stream\DuplexStreamInterface;
use React\Stream\ThroughStream;
use SplObjectStorage;
class Http2Middleware
{
/** @var SplObjectStorage<Http2Connection> Active connections */
private SplObjectStorage $connections;
private array $handlers;
public function __construct(callable ...$httpHandlers)
2024-02-25 00:18:33 +01:00
{
$this->handlers = $httpHandlers;
2024-02-25 00:18:33 +01:00
$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";
// Parse out headers
$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);
}
try {
$connection = $this->setupConnection($request);
}
catch (\Exception $e) {
return Response::plaintext("Error upgrading connection")->withStatus(Response::STATUS_BAD_REQUEST);
}
$headers = [
'Connection' => 'Upgrade',
'Upgrade' => $requestUpgrade
];
return (new Response(Response::STATUS_SWITCHING_PROTOCOLS, $headers, $connection->getResponseStream()));
2024-02-25 00:18:33 +01:00
}
/**
* Parse the settings frame present in the HTTP/1.1 upgrade request.
*
* @param string $settings
* @return SettingsFrame
*/
2024-02-25 00:18:33 +01:00
private function parseSettingsFromBase64String(string $settings): SettingsFrame
{
$decoded = base64_decode($settings);
$frame = new SettingsFrame();
$frame->fromBinary($decoded);
2024-02-25 00:18:33 +01:00
return $frame;
}
/**
* Prepare a connection for the HTTP/2 session.
*
* @param ServerRequestInterface $request
* @return Http2Connection
*/
2024-02-25 00:18:33 +01:00
private function setupConnection(ServerRequestInterface $request): Http2Connection
{
// handle HTTP/2 upgrade from HTTP/1.1
$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);
2024-02-25 00:18:33 +01:00
$this->connections->attach($connection);
$connection->on('close', function () use ($connection) {
$this->connections->detach($connection);
});
return $connection;
}
}