diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index ca9d57f7..a0c32d20 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -12,26 +12,55 @@ */ class RequestHeaderParser extends EventEmitter { + /** + * While HTTP does not strictly specify max length for headers, + * most HTTP-server implementations use 8K. There are some using + * 4K though (eg nginx use system page size, that is usually around + * 4096 bytes) + */ + const DEFAULT_HEADER_SIZE = 4096; + + /** + * Data buffer used to emit "data" event if we have read + * part of body while seeking headers end (\r\n\r\n) + * + * @var string + */ private $buffer = ''; - private $maxSize = 4096; + + /** + * Maximum headers length + * + * @var int + */ + private $maxSize; + + /** + * @param int $maxSize + */ + public function __construct( + $maxSize = self::DEFAULT_HEADER_SIZE + ) { + $this->maxSize = $maxSize; + } public function feed($data) { - if (strlen($this->buffer) + strlen($data) > $this->maxSize) { - $this->emit('error', array(new \OverflowException("Maximum header size of {$this->maxSize} exceeded."), $this)); - $this->removeAllListeners(); - return; - } + try { + $this->buffer .= $data; - $this->buffer .= $data; + $headerLen = strpos($this->buffer, "\r\n\r\n"); - if (false !== strpos($this->buffer, "\r\n\r\n")) { - try { - $this->parseAndEmitRequest(); - } catch (Exception $exception) { - $this->emit('error', [$exception]); + if (($headerLen ?: strlen($this->buffer)) > $this->maxSize) { + throw new \OverflowException("Maximum header size of {$this->maxSize} exceeded."); } + if (false !== $headerLen) { + $this->parseAndEmitRequest(); + $this->removeAllListeners(); + } + } catch (Exception $exception) { + $this->emit('error', [$exception, $this]); $this->removeAllListeners(); } } @@ -72,4 +101,12 @@ public function parseRequest($data) return array($request, $bodyBuffer); } + + /** + * @return int + */ + public function getMaxHeadersSize() + { + return $this->maxSize; + } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 807af42c..1f2bf156 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -96,12 +96,16 @@ public function testHeadersEventShouldParsePathAndQueryString() $this->assertSame($headers, $request->getHeaders()); } - public function testHeaderOverflowShouldEmitError() - { - $error = null; - $passedParser = null; + /** + * @param RequestHeaderParser $parser + * @param string $data + */ + private function checkHeaderOverflowShouldEmitError( + RequestHeaderParser $parser, + $data + ) { + $error = $passedParser = null; - $parser = new RequestHeaderParser(); $parser->on('headers', $this->expectCallableNever()); $parser->on('error', function ($message, $parser) use (&$error, &$passedParser) { $error = $message; @@ -111,16 +115,37 @@ public function testHeaderOverflowShouldEmitError() $this->assertSame(1, count($parser->listeners('headers'))); $this->assertSame(1, count($parser->listeners('error'))); - $data = str_repeat('A', 4097); $parser->feed($data); $this->assertInstanceOf('OverflowException', $error); - $this->assertSame('Maximum header size of 4096 exceeded.', $error->getMessage()); + $this->assertSame( + sprintf('Maximum header size of %d exceeded.', $parser->getMaxHeadersSize()), + $error->getMessage() + ); $this->assertSame($parser, $passedParser); $this->assertSame(0, count($parser->listeners('headers'))); $this->assertSame(0, count($parser->listeners('error'))); } + public function testHeaderOverflowShouldEmitErrorDefault() + { + /* + * This checks that exception is thrown if buffer exceeds header + * max length, but header end is still missing + */ + $data = str_repeat('A', RequestHeaderParser::DEFAULT_HEADER_SIZE + 1); + $this->checkHeaderOverflowShouldEmitError(new RequestHeaderParser(), $data); + + /* + * This checks that exception is thrown if header size really exceeded + */ + $data = str_pad($data . "\r\n\r\n", RequestHeaderParser::DEFAULT_HEADER_SIZE * 2, 'B'); + $this->checkHeaderOverflowShouldEmitError(new RequestHeaderParser(), $data); + + $data = str_repeat('A', 8096 + 1); + $this->checkHeaderOverflowShouldEmitError(new RequestHeaderParser(8096), $data); + } + public function testGuzzleRequestParseException() { $error = null; @@ -142,6 +167,31 @@ public function testGuzzleRequestParseException() $this->assertSame(0, count($parser->listeners('error'))); } + /** + * Checks if HeaderParser supports data chunks of size + * bigger, than @see RequestHeaderParser::$maxSize option + * + * @url https://github.com/reactphp/http/issues/80 + */ + public function testHugeBodyFirstChunk() + { + $bodyBuffer = null; + + $parser = new RequestHeaderParser(); + $parser->on('headers', function ($parsedRequest, $parsedBodyBuffer) use (&$bodyBuffer) { + $bodyBuffer = $parsedBodyBuffer; + }); + $parser->on('error', $this->expectCallableNever()); + + $data = $this->createGetRequest(); + $headLength = strlen($data); + $totalLength = 4096 * 16; + $data = str_pad($data, $totalLength, "\0x00"); + $parser->feed($data); + + $this->assertEquals($totalLength - $headLength, strlen($bodyBuffer)); + } + private function createGetRequest() { $data = "GET / HTTP/1.1\r\n";