Skip to content
Merged
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
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