diff --git a/src/Driver/DefaultHttpDriverFactory.php b/src/Driver/DefaultHttpDriverFactory.php index a937d37a..bada9823 100644 --- a/src/Driver/DefaultHttpDriverFactory.php +++ b/src/Driver/DefaultHttpDriverFactory.php @@ -9,6 +9,7 @@ final class DefaultHttpDriverFactory implements HttpDriverFactory { /** + * @param positive-int $headerSizeLimit * @param bool $allowHttp2Upgrade Requires HTTP/2 support to be enabled. * @param bool $pushEnabled Requires HTTP/2 support to be enabled. */ diff --git a/src/Driver/Http1Driver.php b/src/Driver/Http1Driver.php index a0afc1e0..60473044 100644 --- a/src/Driver/Http1Driver.php +++ b/src/Driver/Http1Driver.php @@ -63,6 +63,9 @@ private static function makeHeaderReduceClosure(string $search): \Closure private readonly DeferredCancellation $deferredCancellation; + /** + * @param positive-int $headerSizeLimit + */ public function __construct( RequestHandler $requestHandler, ErrorHandler $errorHandler, diff --git a/src/Driver/Http2Driver.php b/src/Driver/Http2Driver.php index 793a800f..1955be51 100644 --- a/src/Driver/Http2Driver.php +++ b/src/Driver/Http2Driver.php @@ -124,6 +124,9 @@ final class Http2Driver extends AbstractHttpDriver implements Http2Processor private readonly HPack $hpack; + /** + * @param positive-int $headerSizeLimit + */ public function __construct( RequestHandler $requestHandler, ErrorHandler $errorHandler, @@ -240,7 +243,7 @@ private function processClientInput(?string $chunk = null): void $this->client->getId(), )) || true); - $parser = new Http2Parser($this, $this->hpack, $this->settings); + $parser = new Http2Parser($this, $this->hpack, $this->settings, $this->headerSizeLimit); try { $parser->push($chunk ?? $this->readPreface()); diff --git a/test/Driver/Http2DriverTest.php b/test/Driver/Http2DriverTest.php index 4e290826..c9344e02 100644 --- a/test/Driver/Http2DriverTest.php +++ b/test/Driver/Http2DriverTest.php @@ -1087,6 +1087,107 @@ public function testSendingLargeHeaders(): void self::assertSame($value, $request->getHeader($header)); } + public function testAcceptsHeadersUpToTheDefaultHeaderSizeLimit(): void + { + $header = 'x-long-header'; + $value = self::incompressibleString(20000); + + $headers = [ + ":authority" => ["localhost:8888"], + ":path" => ["/foo"], + ":scheme" => ["http"], + ":method" => ["GET"], + $header => $value, + ]; + + $input = Http2Parser::PREFACE; + $input .= self::packHeader($headers); + + $this->givenInput(new ReadableBuffer($input)); + + $request = $this->whenRequestIsReceived(); + + self::assertSame($value, $request->getHeader($header)); + } + + public function testEnforcesTheConfiguredHeaderSizeLimit(): void + { + $this->initDriver(['headerSizeLimit' => 4096]); + + $headers = [ + ":authority" => ["localhost:8888"], + ":path" => ["/foo"], + ":scheme" => ["http"], + ":method" => ["GET"], + 'x-long-header' => self::incompressibleString(8000), + ]; + + $input = Http2Parser::PREFACE; + $input .= self::packHeader($headers); + + $this->givenInput(new ReadableBuffer($input)); + + $this->whenClientIsHandled(); + + $frames = self::unpackFrames(buffer($this->output->getSource())); + + $settings = \array_values(\array_filter( + $frames, + fn (array $frame) => $frame['type'] === Http2Parser::SETTINGS && !($frame['flags'] & Http2Parser::ACK), + )); + self::assertNotEmpty($settings); + + $advertised = null; + foreach (\str_split($settings[0]['payload'], 6) as $entry) { + ['id' => $id, 'value' => $value] = \unpack('nid/Nvalue', $entry); + if ($id === Http2Parser::MAX_HEADER_LIST_SIZE) { + $advertised = $value; + } + } + self::assertSame(4096, $advertised); + + $goAway = \array_values(\array_filter( + $frames, + fn (array $frame) => $frame['type'] === Http2Parser::GOAWAY, + )); + self::assertCount(1, $goAway); + self::assertSame(Http2Parser::COMPRESSION_ERROR, \unpack('N', $goAway[0]['payload'], 4)[1]); + self::assertSame([], $this->requests); + } + + private static function incompressibleString(int $length): string + { + $string = ''; + for ($i = 0; \strlen($string) < $length; ++$i) { + $string .= \hash('sha512', (string) $i); + } + + return \substr($string, 0, $length); + } + + /** + * @return list + */ + private static function unpackFrames(string $buffer): array + { + $frames = []; + $offset = 0; + + while (\strlen($buffer) - $offset >= 9) { + ['length' => $length, 'type' => $type, 'flags' => $flags, 'stream' => $stream] + = \unpack('Nlength/Ctype/Cflags/Nstream', "\0" . \substr($buffer, $offset, 9)); + $frames[] = [ + 'type' => $type, + 'flags' => $flags, + 'stream' => $stream & 0x7fffffff, + 'payload' => \substr($buffer, $offset + 9, $length), + ]; + $offset += 9 + $length; + } + + return $frames; + } + public function testTimeoutSuspendedDuringRequestHandler(): void { $requestHandler = new ClosureRequestHandler(function (): Response {