Lots of frame stubs

This commit is contained in:
Chris 2024-02-25 00:18:33 +01:00
parent fb1ce8cbc1
commit ff824cdaba
14 changed files with 401 additions and 1 deletions

View File

@ -1,6 +1,6 @@
# HTTP/2 Support for ReactPHP
This is a project that is exploring the feasability and practicality of bringing HTTP/2 to ReactPHP while staying true to the ReactPHP philosophy. The main rationale for this is to eventually enable native support for gRPC.
This is a project that is exploring the feasability and practicality of bringing HTTP/2 to ReactPHP while staying true to the ReactPHP philosophy. The main rationale for this is to eventually enable native support for gRPC. Plus it is neat!
## Status
@ -8,3 +8,35 @@ Currently implemented:
* Huffman encoding (for compressed headers)
* Header parsing (including dictionary)
* Frame parsing (not all frame types)
## Notes
* As HTTP/2 is multiplexed, use of promises will be essential for the library as well as any client code. Without promises, all requests will be blocking and synchronous, which isn't necessarily bad but still wasteful.
## Mockup
The idea is tho end up with something like this. Expect a lot to change tho.
```php
// Tradtional request handler
$http = function (Psr\Http\Message\ServerRequestInterface $request, ?callable $next=null) {
$promise = new React\Promise\Promise();
React\Loop\Loop::laterTick(function () use ($request, $promise) {
// ...
$promise->resolve($response);
});
return $promise;
};
// HTTP/2 request handler
$http2 = new NoccyLabs\React\Http2\Http2Middleware($http);
// React HTTP sever
$server = new React\Http\HttpServer(
$http2,
$http,
)
```

View File

@ -0,0 +1,53 @@
<?php
namespace NoccyLabs\React\Http2\Connection;
use Evenement\EventEmitterInterface;
use Evenement\EventEmitterTrait;
use NoccyLabs\React\Http2\Stream\Http2Stream;
use React\Stream\DuplexStreamInterface;
/**
* HTTP/2 Connecton Classs
*
*
*/
class Http2Connection implements EventEmitterInterface
{
use EventEmitterTrait;
/** @var string Emitted when a new stream is opened */
const EVENT_OPEN = 'open';
/** @var string Emitted when a the connection (and effectively all its streams) are closed */
const EVENT_CLOSE = 'close';
/** @var array<int,Http2Stream> The active streams, even for s2c, odd for c2s */
private array $streams = [];
/** @var int The last used stream identifier */
private int $lastStream = 0;
/** @var int Max number of streams that can be opened; assigned from settings frame */
private int $maxStream = 0;
/** @var DuplexStreamInterface The connection to the client */
private DuplexStreamInterface $connection;
public function __construct(DuplexStreamInterface $connection)
{
$this->connection = $connection;
}
/**
* When a new stream is opened, clean up older streams that are idle or closed.
*
* @return void
*/
private function doStreamHouseKeeping(): void
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class ContinuationFrame extends Frame
{
}

8
src/Frame/DataFrame.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class DataFrame extends Frame
{
}

125
src/Frame/Frame.php Normal file
View File

@ -0,0 +1,125 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
abstract class Frame
{
const FRAME_DATA = 0x0;
const FRAME_HEADERS = 0x1;
const FRAME_PRIORITY = 0x2;
const FRAME_SETTINGS = 0x4;
const FRAME_PUSH_PROMISE = 0x5;
const FRAME_PING = 0x6;
const FRAME_GOAWAY = 0x7;
const FRAME_WINDOW_UPDATE = 0x8;
const FRAME_CONTINUATION = 0x9;
public static $frameMap = [
self::FRAME_DATA => DataFrame::class,
self::FRAME_HEADERS => HeadersFrame::class,
self::FRAME_PRIORITY => PriorityFrame::class,
self::FRAME_SETTINGS => SettingsFrame::class,
self::FRAME_PUSH_PROMISE => PushPromiseFrame::class,
self::FRAME_PING => PingFrame::class,
self::FRAME_GOAWAY => GoAwayFrame::class,
self::FRAME_WINDOW_UPDATE => WindowUpdateFrame::class,
self::FRAME_CONTINUATION => ContinuationFrame::class
];
/** @var int The associated condition is not a result of an error. For example, a GOAWAY might include this code to indicate graceful shutdown of a connection. */
const ERROR_NO_ERROR = 0x0;
/** @var int The endpoint detected an unspecific protocol error. This error is for use when a more specific error code is not available. */
const ERROR_PROTOCOL_ERROR = 0x1;
/** @var int The endpoint encountered an unexpected internal error. */
const ERROR_INTERNAL_ERROR = 0x2;
/** @var int The endpoint detected that its peer violated the flow-control protocol. */
const ERROR_FLOW_CONTROL_ERROR = 0x3;
/** @var int The endpoint sent a SETTINGS frame but did not receive a response in a timely manner. See Section 6.5.3 ("Settings Synchronization"). */
const ERROR_SETTINGS_TIMEOUT = 0x4;
/** @var int The endpoint received a frame after a stream was half-closed. */
const ERROR_STREAM_CLOSED = 0x5;
/** @var int The endpoint received a frame with an invalid size. */
const ERROR_FRAME_SIZE_ERROR = 0x6;
/** @var int The endpoint refused the stream prior to performing any application processing (see Section 8.1.4 for details). */
const ERROR_REFUSED_STREAM = 0x7;
/** @var int Used by the endpoint to indicate that the stream is no longer needed. */
const ERROR_CANCEL = 0x8;
/** @var int The endpoint is unable to maintain the header compression context for the connection. */
const ERROR_COMPRESSION_ERROR = 0x9;
/** @var int The connection established in response to aCONNECT request (Section 8.3) was reset or abnormally closed. */
const ERROR_CONNECT_ERROR = 0xa;
/** @var int The endpoint detected that its peer is exhibiting a behavior that might be generating excessive load. */
const ERROR_ENHANCE_YOUR_CALM = 0xb;
/** @var int The underlying transport has properties that do not meet minimum security requirements (see Section 9.2). */
const ERROR_INADEQUATE_SECURITY = 0xc;
/** @var int The endpoint requires that HTTP/1.1 be used instead of HTTP/2. */
const ERROR_HTTP_1_1_REQUIRED = 0xd;
protected int $frameType;
protected int $frameFlags;
protected int $streamIdentifier;
public function getFrameType(): int
{
return $this->frameType;
}
abstract public function toBinary(): string;
abstract protected function fromBinary(string $binary): void;
public function encodeFrame(): string
{
$frame = '';
// TODO build header
$payload = $this->toBinary();
$length = strlen($payload);
$frame .= chr(($length >> 16) & 0xFF)
. chr(($length >> 8) & 0xFF)
. chr(($length) & 0xFF)
. chr($this->frameType)
. chr($this->frameFlags)
. chr(($this->streamIdentifier >> 24) & 0x7F)
. chr(($this->streamIdentifier >> 16) & 0xFF)
. chr(($this->streamIdentifier >> 8) & 0xFF)
. chr(($this->streamIdentifier) & 0xFF)
. $payload;
return $frame;
}
public static function parseFrame(string &$data): Frame
{
$header = array_map('ord', str_split(substr($data,0,9)));
$length = ($header[0] << 16)
| ($header[1] << 8)
| ($header[2]);
$type = $header[3];
$flags = $header[4];
$stream = (($header[5] & 0x7F) << 24)
| ($header[6] << 16)
| ($header[7] << 8)
| ($header[8]);
if (!array_key_exists($type, self::$frameMap)) {
// TODO handle this
}
/** @var Frame $frame */
$frame = new self::$frameMap[$type];
$frame->frameType = $type;
$frame->frameFlags = $flags;
$frame->streamIdentifier = $stream;
// Grab the payload and parse it
$payload = substr($data, 9, $length);
$frame->fromBinary($payload);
return $frame;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class GoAwayFrame extends Frame
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class HeadersFrame extends Frame
{
}

8
src/Frame/PingFrame.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class PingFrame extends Frame
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class PriorityFrame extends Frame
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class PushPromiseFrame extends Frame
{
}

View File

@ -0,0 +1,51 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class SettingsFrame extends Frame
{
const SETTINGS_HEADER_TABLE_SIZE = 0x1;
const SETTINGS_ENABLE_PUSH = 0x2;
const SETTINGS_MAX_CONCURRENT_STREAMS = 0x3;
const SETTINGS_INITIAL_WINDOW_SIZE = 0x4;
const SETTINGS_MAX_FRAME_SIZE = 0x5;
const SETTINGS_MAX_HEADER_LIST_SIZE = 0x6;
protected int $frameType = self::FRAME_SETTINGS;
protected array $settings = [];
/**
* Set a setting to a specific value. This method intentionally does not do any bounds
* checking on the setting id to allow for extensions.
*
* @param int $setting
* @param int $value
* @return self
*/
public function set(int $setting, int $value): self
{
$this->settings[$setting] = $value;
return $this;
}
public function get(int $setting): ?int
{
return $this->settings[$setting]??null;
}
public function toBinary(): string
{
$packed = '';
foreach ($this->settings as $setting=>$value) {
$packed .= pack('vV', $setting, $value);
}
return $packed;
}
protected function fromBinary(string $data): void
{
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\React\Http2\Frame;
class WindowUpdateFrame extends Frame
{
}

48
src/Http2Middleware.php Normal file
View File

@ -0,0 +1,48 @@
<?php
namespace NoccyLabs\React\Http2;
use NoccyLabs\React\Http2\Connection\Http2Connection;
use NoccyLabs\React\Http2\Frame\SettingsFrame;
use Psr\Http\Message\ServerRequestInterface;
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)
{
}
private function parseSettingsFromBase64String(string $settings): SettingsFrame
{
$decoded = base64_decode($settings);
$frame = new SettingsFrame();
$frame->parseFrame($decoded);
return $frame;
}
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;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace NoccyLabs\React\Http2\Stream;
use NoccyLabs\React\Http2\Connection\Http2Connection;
/**
* HTTP/2 Stream Class
*
*
*
*/
class Http2Stream
{
const STATE_IDLE = 0;
const STATE_RESERVED_LOCAL = 1;
const STATE_RESERVED_REMOTE = 2;
const STATE_OPEN = 3;
const STATE_HALF_CLOSED_LOCAL = 4;
const STATE_HALF_CLOSED_REMOTE = 5;
const STATE_CLOSED = 6;
private int $state = self::STATE_IDLE;
private Http2Connection $connection;
}