diff --git a/README.md b/README.md index 002acc1..55fe406 100644 --- a/README.md +++ b/README.md @@ -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, +) + +``` diff --git a/src/Connection/Http2Connection.php b/src/Connection/Http2Connection.php new file mode 100644 index 0000000..f8ec46e --- /dev/null +++ b/src/Connection/Http2Connection.php @@ -0,0 +1,53 @@ + 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 + { + + } +} \ No newline at end of file diff --git a/src/Frame/ContinuationFrame.php b/src/Frame/ContinuationFrame.php new file mode 100644 index 0000000..8f16006 --- /dev/null +++ b/src/Frame/ContinuationFrame.php @@ -0,0 +1,8 @@ + 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; + } + +} + diff --git a/src/Frame/GoAwayFrame.php b/src/Frame/GoAwayFrame.php new file mode 100644 index 0000000..435ca88 --- /dev/null +++ b/src/Frame/GoAwayFrame.php @@ -0,0 +1,8 @@ +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 + { + + } +} + diff --git a/src/Frame/WindowUpdateFrame.php b/src/Frame/WindowUpdateFrame.php new file mode 100644 index 0000000..5dc18a0 --- /dev/null +++ b/src/Frame/WindowUpdateFrame.php @@ -0,0 +1,8 @@ + 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; + } +} \ No newline at end of file diff --git a/src/Stream/Http2Stream.php b/src/Stream/Http2Stream.php new file mode 100644 index 0000000..0ec96fd --- /dev/null +++ b/src/Stream/Http2Stream.php @@ -0,0 +1,27 @@ +