diff --git a/README.md b/README.md index 3396c345..c827ffd3 100644 --- a/README.md +++ b/README.md @@ -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([ @@ -719,7 +719,7 @@ 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); }, ]); @@ -727,7 +727,7 @@ $middlewares = new MiddlewareRunner([ #### 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 @@ -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 = [ @@ -771,7 +771,7 @@ $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); }, ]); @@ -779,7 +779,7 @@ $middlewares = new MiddlewareRunner([ #### 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 diff --git a/examples/80-request-header-parser.php b/examples/80-request-header-parser.php new file mode 100644 index 00000000..b449787f --- /dev/null +++ b/examples/80-request-header-parser.php @@ -0,0 +1,43 @@ +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(); diff --git a/src/RequestHeaderParser.php b/src/RequestHeaderParser.php index a6126da9..71e55393 100644 --- a/src/RequestHeaderParser.php +++ b/src/RequestHeaderParser.php @@ -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) diff --git a/src/RequestHeaderParserFactory.php b/src/RequestHeaderParserFactory.php new file mode 100644 index 00000000..78b504ff --- /dev/null +++ b/src/RequestHeaderParserFactory.php @@ -0,0 +1,80 @@ +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']); + } + +} diff --git a/src/RequestHeaderParserFactoryInterface.php b/src/RequestHeaderParserFactoryInterface.php new file mode 100644 index 00000000..c2a98371 --- /dev/null +++ b/src/RequestHeaderParserFactoryInterface.php @@ -0,0 +1,15 @@ +callback = $callback; + $this->factory = $factory ? $factory : new RequestHeaderParserFactory(); } /** @@ -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) { @@ -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']); - } } diff --git a/tests/RequestHeaderParserTest.php b/tests/RequestHeaderParserTest.php index 550b0934..52e7e206 100644 --- a/tests/RequestHeaderParserTest.php +++ b/tests/RequestHeaderParserTest.php @@ -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(); @@ -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; diff --git a/tests/ServerTest.php b/tests/ServerTest.php index bdeb759d..bef50660 100644 --- a/tests/ServerTest.php +++ b/tests/ServerTest.php @@ -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) { @@ -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);