2024-02-25 00:18:33 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace NoccyLabs\React\Http2;
|
|
|
|
|
|
|
|
use NoccyLabs\React\Http2\Connection\Http2Connection;
|
2024-07-27 13:23:18 +02:00
|
|
|
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;
|
2024-07-27 13:23:18 +02:00
|
|
|
use NoccyLabs\React\Http2\Header\HeaderPacker;
|
2024-02-25 00:18:33 +01:00
|
|
|
use Psr\Http\Message\ServerRequestInterface;
|
2024-07-27 13:23:18 +02:00
|
|
|
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;
|
|
|
|
|
2024-07-27 16:21:24 +02:00
|
|
|
private array $handlers;
|
|
|
|
|
|
|
|
public function __construct(callable ...$httpHandlers)
|
2024-02-25 00:18:33 +01:00
|
|
|
{
|
2024-07-27 16:21:24 +02:00
|
|
|
$this->handlers = $httpHandlers;
|
2024-02-25 00:18:33 +01:00
|
|
|
$this->connections = new SplObjectStorage();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function __invoke(ServerRequestInterface $request, ?callable $next=null)
|
|
|
|
{
|
2024-02-27 00:21:54 +01:00
|
|
|
// expect upgrade h2 for secure connections, h2c for plaintext
|
2024-07-27 13:23:18 +02:00
|
|
|
$requestSecure = $request->getHeaderLine("x-forwarded-proto") ?: "http";
|
2024-07-27 16:21:24 +02:00
|
|
|
|
|
|
|
// Parse out headers
|
2024-07-27 13:23:18 +02:00
|
|
|
$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);
|
|
|
|
}
|
|
|
|
|
2024-07-27 16:21:24 +02:00
|
|
|
try {
|
|
|
|
$connection = $this->setupConnection($request);
|
|
|
|
}
|
|
|
|
catch (\Exception $e) {
|
|
|
|
return Response::plaintext("Error upgrading connection")->withStatus(Response::STATUS_BAD_REQUEST);
|
2024-07-27 13:23:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$headers = [
|
|
|
|
'Connection' => 'Upgrade',
|
|
|
|
'Upgrade' => $requestUpgrade
|
|
|
|
];
|
|
|
|
|
2024-07-27 16:21:24 +02:00
|
|
|
return (new Response(Response::STATUS_SWITCHING_PROTOCOLS, $headers, $connection->getResponseStream()));
|
2024-02-25 00:18:33 +01:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2024-07-27 13:23:18 +02:00
|
|
|
|
2024-02-27 00:21:54 +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();
|
2024-02-27 00:21:54 +01:00
|
|
|
$frame->fromBinary($decoded);
|
2024-02-25 00:18:33 +01:00
|
|
|
return $frame;
|
|
|
|
}
|
|
|
|
|
2024-02-27 00:21:54 +01:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
{
|
2024-07-27 16:21:24 +02:00
|
|
|
// 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;
|
|
|
|
}
|
2024-07-27 13:23:18 +02:00
|
|
|
}
|