Skip to content

Commit

Permalink
Merge pull request #349 from clue-labs/optimize-parser
Browse files Browse the repository at this point in the history
Refactor and optimize parsing request
  • Loading branch information
jsor committed Sep 26, 2019
2 parents f30fb12 + 658ca68 commit 369d495
Show file tree
Hide file tree
Showing 6 changed files with 556 additions and 377 deletions.
142 changes: 142 additions & 0 deletions src/Io/EmptyBodyStream.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

namespace React\Http\Io;

use Evenement\EventEmitter;
use Psr\Http\Message\StreamInterface;
use React\Stream\ReadableStreamInterface;
use React\Stream\Util;
use React\Stream\WritableStreamInterface;

/**
* [Internal] Bridge between an empty StreamInterface from PSR-7 and ReadableStreamInterface from ReactPHP
*
* This class is used in the server to represent an empty body stream of an
* incoming response from the client. This is similar to the `HttpBodyStream`,
* but is specifically designed for the common case of having an empty message
* body.
*
* Note that this is an internal class only and nothing you should usually care
* about. See the `StreamInterface` and `ReadableStreamInterface` for more
* details.
*
* @see HttpBodyStream
* @see StreamInterface
* @see ReadableStreamInterface
* @internal
*/
class EmptyBodyStream extends EventEmitter implements StreamInterface, ReadableStreamInterface
{
private $closed = false;

public function isReadable()
{
return !$this->closed;
}

public function pause()
{
// NOOP
}

public function resume()
{
// NOOP
}

public function pipe(WritableStreamInterface $dest, array $options = array())
{
Util::pipe($this, $dest, $options);

return $dest;
}

public function close()
{
if ($this->closed) {
return;
}

$this->closed = true;

$this->emit('close');
$this->removeAllListeners();
}

public function getSize()
{
return 0;
}

/** @ignore */
public function __toString()
{
return '';
}

/** @ignore */
public function detach()
{
return null;
}

/** @ignore */
public function tell()
{
throw new \BadMethodCallException();
}

/** @ignore */
public function eof()
{
throw new \BadMethodCallException();
}

/** @ignore */
public function isSeekable()
{
return false;
}

/** @ignore */
public function seek($offset, $whence = SEEK_SET)
{
throw new \BadMethodCallException();
}

/** @ignore */
public function rewind()
{
throw new \BadMethodCallException();
}

/** @ignore */
public function isWritable()
{
return false;
}

/** @ignore */
public function write($string)
{
throw new \BadMethodCallException();
}

/** @ignore */
public function read($length)
{
throw new \BadMethodCallException();
}

/** @ignore */
public function getContents()
{
return '';
}

/** @ignore */
public function getMetadata($key = null)
{
return ($key === null) ? array() : null;
}
}
57 changes: 55 additions & 2 deletions src/Io/RequestHeaderParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,42 @@ public function handle(ConnectionInterface $conn)
return;
}

$contentLength = 0;
if ($request->hasHeader('Transfer-Encoding')) {
$contentLength = null;
} elseif ($request->hasHeader('Content-Length')) {
$contentLength = (int)$request->getHeaderLine('Content-Length');
}

if ($contentLength === 0) {
// happy path: request body is known to be empty
$stream = new EmptyBodyStream();
$request = $request->withBody($stream);
} else {
// otherwise body is present => delimit using Content-Length or ChunkedDecoder
$stream = new CloseProtectionStream($conn);
if ($contentLength !== null) {
$stream = new LengthLimitedStream($stream, $contentLength);
} else {
$stream = new ChunkedDecoder($stream);
}

$request = $request->withBody(new HttpBodyStream($stream, $contentLength));
}

$bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : '';
$buffer = '';
$that->emit('headers', array($request, $bodyBuffer, $conn));
$that->emit('headers', array($request, $conn));

if ($bodyBuffer !== '') {
$conn->emit('data', array($bodyBuffer));
}

// happy path: request body is known to be empty => immediately end stream
if ($contentLength === 0) {
$stream->emit('end');
$stream->close();
}
});

$conn->on('close', function () use (&$buffer, &$fn) {
Expand Down Expand Up @@ -135,7 +168,7 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)

// apply SERVER_ADDR and SERVER_PORT if server address is known
// address should always be known, even for Unix domain sockets (UDS)
// but skip UDS as it doesn't have a concept of host/port.s
// but skip UDS as it doesn't have a concept of host/port.
if ($localSocketUri !== null) {
$localAddress = \parse_url($localSocketUri);
if (isset($localAddress['host'], $localAddress['port'])) {
Expand Down Expand Up @@ -199,6 +232,26 @@ public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
}
}

// ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers
if ($request->hasHeader('Transfer-Encoding')) {
if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') {
throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', 501);
}

// Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time
// as per https://tools.ietf.org/html/rfc7230#section-3.3.3
if ($request->hasHeader('Content-Length')) {
throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', 400);
}
} elseif ($request->hasHeader('Content-Length')) {
$string = $request->getHeaderLine('Content-Length');

if ((string)(int)$string !== $string) {
// Content-Length value is not an integer or not a single integer
throw new \InvalidArgumentException('The value of `Content-Length` is not valid', 400);
}
}

// set URI components from socket address if not already filled via Host header
if ($request->getUri()->getHost() === '') {
$parts = \parse_url($localSocketUri);
Expand Down
44 changes: 1 addition & 43 deletions src/StreamingServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,12 +112,8 @@ public function __construct($requestHandler)
$this->parser = new RequestHeaderParser();

$that = $this;
$this->parser->on('headers', function (ServerRequestInterface $request, $bodyBuffer, ConnectionInterface $conn) use ($that) {
$this->parser->on('headers', function (ServerRequestInterface $request, ConnectionInterface $conn) use ($that) {
$that->handleRequest($conn, $request);

if ($bodyBuffer !== '') {
$conn->emit('data', array($bodyBuffer));
}
});

$this->parser->on('error', function(\Exception $e, ConnectionInterface $conn) use ($that) {
Expand Down Expand Up @@ -182,38 +178,6 @@ public function listen(ServerInterface $socket)
/** @internal */
public function handleRequest(ConnectionInterface $conn, ServerRequestInterface $request)
{
$contentLength = 0;
$stream = new CloseProtectionStream($conn);
if ($request->hasHeader('Transfer-Encoding')) {
if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') {
$this->emit('error', array(new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding')));
return $this->writeError($conn, 501, $request);
}

// Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time
// as per https://tools.ietf.org/html/rfc7230#section-3.3.3
if ($request->hasHeader('Content-Length')) {
$this->emit('error', array(new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed')));
return $this->writeError($conn, 400, $request);
}

$stream = new ChunkedDecoder($stream);
$contentLength = null;
} elseif ($request->hasHeader('Content-Length')) {
$string = $request->getHeaderLine('Content-Length');

$contentLength = (int)$string;
if ((string)$contentLength !== $string) {
// Content-Length value is not an integer or not a single integer
$this->emit('error', array(new \InvalidArgumentException('The value of `Content-Length` is not valid')));
return $this->writeError($conn, 400, $request);
}

$stream = new LengthLimitedStream($stream, $contentLength);
}

$request = $request->withBody(new HttpBodyStream($stream, $contentLength));

if ($request->getProtocolVersion() !== '1.0' && '100-continue' === \strtolower($request->getHeaderLine('Expect'))) {
$conn->write("HTTP/1.1 100 Continue\r\n\r\n");
}
Expand All @@ -237,12 +201,6 @@ public function handleRequest(ConnectionInterface $conn, ServerRequestInterface
});
}

// happy path: request body is known to be empty => immediately end stream
if ($contentLength === 0) {
$stream->emit('end');
$stream->close();
}

// happy path: response returned, handle and return immediately
if ($response instanceof ResponseInterface) {
return $this->handleResponse($conn, $request, $response);
Expand Down
Loading

0 comments on commit 369d495

Please sign in to comment.