Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 49 additions & 12 deletions src/RequestHeaderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -72,4 +101,12 @@ public function parseRequest($data)

return array($request, $bodyBuffer);
}

/**
* @return int
*/
public function getMaxHeadersSize()
{
return $this->maxSize;
}
}
64 changes: 57 additions & 7 deletions tests/RequestHeaderParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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";
Expand Down