Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to define a custom request header size if you need bigger headers #217

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
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -666,8 +666,8 @@ instead of the `callable`. A middleware is expected to adhere the following rule
* It returns a `ResponseInterface` (or any promise which can be consumed by [`Promise\resolve`](http://reactphp.org/promise/#resolve) resolving to a `ResponseInterface`)
* It calls `$next($request)` to continue processing the next middleware function or returns explicitly to abort the chain

The following example adds a middleware that adds the current time to the request as a
header (`Request-Time`) and middleware that always returns a 200 code without a body:
The following example adds a middleware that adds the current time to the request as a
header (`Request-Time`) and middleware that always returns a 200 code without a body:

```php
$server = new Server(new MiddlewareRunner([
Expand Down Expand Up @@ -719,15 +719,15 @@ Usage:
$middlewares = new MiddlewareRunner([
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
function (ServerRequestInterface $request, callable $next) {
// The body from $request->getBody() is now fully available without the need to stream it
// The body from $request->getBody() is now fully available without the need to stream it
return new Response(200);
},
]);
```

#### RequestBodyParserMiddleware

The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)),
The `RequestBodyParserMiddleware` takes a fully buffered request body (generally from [`RequestBodyBufferMiddleware`](#requestbodybuffermiddleware)),
and parses the forms and uploaded files from the request body.

Parsed submitted forms will be available from `$request->getParsedBody()` as
Expand All @@ -752,7 +752,7 @@ also supports `multipart/form-data`, thus supporting uploaded files available
through `$request->getUploadedFiles()`.

The `$request->getUploadedFiles(): array` will return an array with all
uploaded files formatted like this:
uploaded files formatted like this:

```php
$uploadedFiles = [
Expand All @@ -771,15 +771,15 @@ $middlewares = new MiddlewareRunner([
new RequestBodyBufferMiddleware(16 * 1024 * 1024), // 16 MiB
new RequestBodyParserMiddleware(),
function (ServerRequestInterface $request, callable $next) {
// If any, parsed form fields are now available from $request->getParsedBody()
// If any, parsed form fields are now available from $request->getParsedBody()
return new Response(200);
},
]);
```

#### Third-Party Middleware

A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page.
A non-exhaustive list of third-party middleware can be found at the [`Middleware`](https://github.com/reactphp/http/wiki/Middleware) wiki page.

## Install

Expand Down
43 changes: 43 additions & 0 deletions examples/80-request-header-parser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use Psr\Http\Message\ServerRequestInterface;
use React\EventLoop\Factory;
use React\Http\Response;
use React\Http\RequestHeaderParser;
use React\Http\RequestHeaderParserFactory;
use React\Http\Server;
use React\Socket\ConnectionInterface;

require __DIR__ . '/../vendor/autoload.php';

class CustomRequestHeaderSizeFactory extends RequestHeaderParserFactory
{

protected $size;

public function __construct($size = 1024)
{
$this->size = $size;
}

public function create(ConnectionInterface $conn)
{
$uriLocal = $this->getUriLocal($conn);
$uriRemote = $this->getUriRemote($conn);

return new RequestHeaderParser($uriLocal, $uriRemote, $this->size);
}
}

$loop = Factory::create();

$server = new Server(function (ServerRequestInterface $request) {
return new Response(200);
}, new CustomRequestHeaderSizeFactory(1024 * 16)); // 16MB

$socket = new \React\Socket\Server(isset($argv[1]) ? $argv[1] : '0.0.0.0:0', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;

$loop->run();
11 changes: 8 additions & 3 deletions src/RequestHeaderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@
*
* @internal
*/
class RequestHeaderParser extends EventEmitter
class RequestHeaderParser extends EventEmitter implements RequestHeaderParserInterface
{
private $buffer = '';
private $maxSize = 4096;
private $maxSize;

private $localSocketUri;
private $remoteSocketUri;

public function __construct($localSocketUri = null, $remoteSocketUri = null)
public function __construct($localSocketUri = null, $remoteSocketUri = null, $maxSize = 4096)
{
if (!is_integer($maxSize)) {
throw new \InvalidArgumentException('Invalid type for maxSize provided. Expected an integer value.');
}

$this->localSocketUri = $localSocketUri;
$this->remoteSocketUri = $remoteSocketUri;
$this->maxSize = $maxSize;
}

public function feed($data)
Expand Down
80 changes: 80 additions & 0 deletions src/RequestHeaderParserFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php

namespace React\Http;

use React\Socket\ConnectionInterface;

class RequestHeaderParserFactory implements RequestHeaderParserFactoryInterface
{

/**
* @param ConnectionInterface $conn
* @return RequestHeaderParserInterface
*/
public function create(ConnectionInterface $conn)
{
$uriLocal = $this->getUriLocal($conn);
$uriRemote = $this->getUriRemote($conn);

return new RequestHeaderParser($uriLocal, $uriRemote);
}

/**
* @param ConnectionInterface $conn
* @return string
*/
protected function getUriLocal(ConnectionInterface $conn)
{
$uriLocal = $conn->getLocalAddress();
if ($uriLocal !== null && strpos($uriLocal, '://') === false) {
// local URI known but does not contain a scheme. Should only happen for old Socket < 0.8
// try to detect transport encryption and assume default application scheme
$uriLocal = ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $uriLocal;
} elseif ($uriLocal !== null) {
// local URI known, so translate transport scheme to application scheme
$uriLocal = strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://'));
}

return $uriLocal;
}

/**
* @param ConnectionInterface $conn
* @return string
*/
protected function getUriRemote(ConnectionInterface $conn)
{
$uriRemote = $conn->getRemoteAddress();
if ($uriRemote !== null && strpos($uriRemote, '://') === false) {
// local URI known but does not contain a scheme. Should only happen for old Socket < 0.8
// actual scheme is not evaluated but required for parsing URI
$uriRemote = 'unused://' . $uriRemote;
}

return $uriRemote;
}

/**
* @param ConnectionInterface $conn
* @return bool
* @codeCoverageIgnore
*/
private function isConnectionEncrypted(ConnectionInterface $conn)
{
// Legacy PHP < 7 does not offer any direct access to check crypto parameters
// We work around by accessing the context options and assume that only
// secure connections *SHOULD* set the "ssl" context options by default.
if (PHP_VERSION_ID < 70000) {
$context = isset($conn->stream) ? stream_context_get_options($conn->stream) : array();

return (isset($context['ssl']) && $context['ssl']);
}

// Modern PHP 7+ offers more reliable access to check crypto parameters
// by checking stream crypto meta data that is only then made available.
$meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array();

return (isset($meta['crypto']) && $meta['crypto']);
}

}
15 changes: 15 additions & 0 deletions src/RequestHeaderParserFactoryInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace React\Http;

use React\Socket\ConnectionInterface;

interface RequestHeaderParserFactoryInterface
{

/**
* @param ConnectionInterface $conn
* @return RequestHeaderParserInterface
*/
public function create(ConnectionInterface $conn);
}
16 changes: 16 additions & 0 deletions src/RequestHeaderParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace React\Http;

use Evenement\EventEmitterInterface;

interface RequestHeaderParserInterface extends EventEmitterInterface
{

/**
* Feed the RequestHeaderParser with a data chunk from the connection
* @param string $data
* @return void
*/
public function feed($data);
}
51 changes: 9 additions & 42 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ class Server extends EventEmitter
{
private $callback;

/**
* @var RequestHeaderParserFactoryInterface
*/
private $factory;

/**
* Creates an HTTP server that invokes the given callback for each incoming HTTP request
*
Expand All @@ -86,15 +91,17 @@ class Server extends EventEmitter
* See also [listen()](#listen) for more details.
*
* @param callable $callback
* @param RequestHeaderParserFactoryInterface $factory
* @see self::listen()
*/
public function __construct($callback)
public function __construct($callback, RequestHeaderParserFactoryInterface $factory = null)
{
if (!is_callable($callback)) {
throw new \InvalidArgumentException();
}

$this->callback = $callback;
$this->factory = $factory ? $factory : new RequestHeaderParserFactory();
}

/**
Expand Down Expand Up @@ -147,25 +154,8 @@ public function listen(ServerInterface $socket)
/** @internal */
public function handleConnection(ConnectionInterface $conn)
{
$uriLocal = $conn->getLocalAddress();
if ($uriLocal !== null && strpos($uriLocal, '://') === false) {
// local URI known but does not contain a scheme. Should only happen for old Socket < 0.8
// try to detect transport encryption and assume default application scheme
$uriLocal = ($this->isConnectionEncrypted($conn) ? 'https://' : 'http://') . $uriLocal;
} elseif ($uriLocal !== null) {
// local URI known, so translate transport scheme to application scheme
$uriLocal = strtr($uriLocal, array('tcp://' => 'http://', 'tls://' => 'https://'));
}

$uriRemote = $conn->getRemoteAddress();
if ($uriRemote !== null && strpos($uriRemote, '://') === false) {
// local URI known but does not contain a scheme. Should only happen for old Socket < 0.8
// actual scheme is not evaluated but required for parsing URI
$uriRemote = 'unused://' . $uriRemote;
}

$that = $this;
$parser = new RequestHeaderParser($uriLocal, $uriRemote);
$parser = $this->factory->create($conn);

$listener = array($parser, 'feed');
$parser->on('headers', function (RequestInterface $request, $bodyBuffer) use ($conn, $listener, $that) {
Expand Down Expand Up @@ -422,27 +412,4 @@ private function handleResponseBody(ResponseInterface $response, ConnectionInter
$connection->end();
}
}

/**
* @param ConnectionInterface $conn
* @return bool
* @codeCoverageIgnore
*/
private function isConnectionEncrypted(ConnectionInterface $conn)
{
// Legacy PHP < 7 does not offer any direct access to check crypto parameters
// We work around by accessing the context options and assume that only
// secure connections *SHOULD* set the "ssl" context options by default.
if (PHP_VERSION_ID < 70000) {
$context = isset($conn->stream) ? stream_context_get_options($conn->stream) : array();

return (isset($context['ssl']) && $context['ssl']);
}

// Modern PHP 7+ offers more reliable access to check crypto parameters
// by checking stream crypto meta data that is only then made available.
$meta = isset($conn->stream) ? stream_get_meta_data($conn->stream) : array();

return (isset($meta['crypto']) && $meta['crypto']);
}
}
31 changes: 31 additions & 0 deletions tests/RequestHeaderParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

class RequestHeaderParserTest extends TestCase
{

public function testMaxSizeParameterShouldFailOnWrongType()
{
$this->setExpectedException('InvalidArgumentException', 'Invalid type for maxSize provided. Expected an integer value.');

new RequestHeaderParser(null, null, 'abc');
}

public function testSplitShouldHappenOnDoubleCrlf()
{
$parser = new RequestHeaderParser();
Expand Down Expand Up @@ -124,6 +132,29 @@ public function testHeaderEventViaHttpsShouldApplySchemeFromConstructor()
$this->assertEquals('example.com', $request->getHeaderLine('Host'));
}

public function testCustomBufferSizeOverflowShouldEmitError()
{
$error = null;
$passedParser = null;
$newCustomBufferSize = 1024 * 16;

$parser = new RequestHeaderParser(null, null, $newCustomBufferSize);
$parser->on('headers', $this->expectCallableNever());
$parser->on('error', function ($message, $parser) use (&$error, &$passedParser) {
$error = $message;
$passedParser = $parser;
});

$this->assertSame(1, count($parser->listeners('headers')));
$this->assertSame(1, count($parser->listeners('error')));

$data = str_repeat('A', $newCustomBufferSize + 1);
$parser->feed($data);

$this->assertInstanceOf('OverflowException', $error);
$this->assertSame('Maximum header size of ' . $newCustomBufferSize . ' exceeded.', $error->getMessage());
}

public function testHeaderOverflowShouldEmitError()
{
$error = null;
Expand Down
5 changes: 3 additions & 2 deletions tests/ServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1256,8 +1256,9 @@ function ($data) use (&$buffer) {
$this->assertContains("Error 505: HTTP Version not supported", $buffer);
}

public function testRequestOverflowWillEmitErrorAndSendErrorResponse()
public function testRequestHeaderOverflowWithDefaultValueWillEmitErrorAndSendErrorResponse()
{
$defaultMaxHeaderSize = 4096;
$error = null;
$server = new Server($this->expectCallableNever());
$server->on('error', function ($message) use (&$error) {
Expand All @@ -1281,7 +1282,7 @@ function ($data) use (&$buffer) {
$this->socket->emit('connection', array($this->connection));

$data = "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\nX-DATA: ";
$data .= str_repeat('A', 4097 - strlen($data)) . "\r\n\r\n";
$data .= str_repeat('A', $defaultMaxHeaderSize + 1 - strlen($data)) . "\r\n\r\n";
$this->connection->emit('data', array($data));

$this->assertInstanceOf('OverflowException', $error);
Expand Down