Skip to content

Commit

Permalink
Merge pull request #116 from legionth/chunk-encoding
Browse files Browse the repository at this point in the history
Decode chunked transfer encoding for incoming requests
  • Loading branch information
clue authored Feb 19, 2017
2 parents 61a69d7 + 61d7b69 commit 8dfc3a3
Show file tree
Hide file tree
Showing 5 changed files with 757 additions and 3 deletions.
158 changes: 158 additions & 0 deletions src/ChunkedDecoder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<?php
namespace React\Http;

use Evenement\EventEmitter;
use React\Stream\ReadableStreamInterface;
use React\Stream\WritableStreamInterface;
use React\Stream\Util;
use Exception;

/** @internal */
class ChunkedDecoder extends EventEmitter implements ReadableStreamInterface
{
const CRLF = "\r\n";
const MAX_CHUNK_HEADER_SIZE = 1024;

private $closed = false;
private $input;
private $buffer = '';
private $chunkSize = 0;
private $transferredSize = 0;
private $headerCompleted = false;

public function __construct(ReadableStreamInterface $input)
{
$this->input = $input;

$this->input->on('data', array($this, 'handleData'));
$this->input->on('end', array($this, 'handleEnd'));
$this->input->on('error', array($this, 'handleError'));
$this->input->on('close', array($this, 'close'));
}

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

public function pause()
{
$this->input->pause();
}

public function resume()
{
$this->input->resume();
}

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

return $dest;
}

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

$this->buffer = '';

$this->closed = true;

$this->input->close();

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

/** @internal */
public function handleEnd()
{
if (!$this->closed) {
$this->handleError(new \Exception('Unexpected end event'));
}
}

/** @internal */
public function handleError(\Exception $e)
{
$this->emit('error', array($e));
$this->close();
}

/** @internal */
public function handleData($data)
{
$this->buffer .= $data;

while ($this->buffer !== '') {
if (!$this->headerCompleted) {
$positionCrlf = strpos($this->buffer, static::CRLF);

if ($positionCrlf === false) {
// Header shouldn't be bigger than 1024 bytes
if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) {
$this->handleError(new \Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes'));
}
return;
}

$header = strtolower((string)substr($this->buffer, 0, $positionCrlf));
$hexValue = $header;

if (strpos($header, ';') !== false) {
$array = explode(';', $header);
$hexValue = $array[0];
}

$this->chunkSize = hexdec($hexValue);
if (dechex($this->chunkSize) !== $hexValue) {
$this->handleError(new \Exception($hexValue . ' is not a valid hexadecimal number'));
return;
}

$this->buffer = (string)substr($this->buffer, $positionCrlf + 2);
$this->headerCompleted = true;
if ($this->buffer === '') {
return;
}
}

$chunk = (string)substr($this->buffer, 0, $this->chunkSize - $this->transferredSize);

if ($chunk !== '') {
$this->transferredSize += strlen($chunk);
$this->emit('data', array($chunk));
$this->buffer = (string)substr($this->buffer, strlen($chunk));
}

$positionCrlf = strpos($this->buffer, static::CRLF);

if ($positionCrlf === 0) {
if ($this->chunkSize === 0) {
$this->emit('end');
$this->close();
return;
}
$this->chunkSize = 0;
$this->headerCompleted = false;
$this->transferredSize = 0;
$this->buffer = (string)substr($this->buffer, 2);
}

if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && strlen($this->buffer) > 2) {
// the first 2 characters are not CLRF, send error event
$this->handleError(new \Exception('Chunk does not end with a CLRF'));
return;
}

if ($positionCrlf !== 0 && strlen($this->buffer) < 2) {
// No CLRF found, wait for additional data which could be a CLRF
return;
}
}
}
}
16 changes: 13 additions & 3 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public function handleConnection(ConnectionInterface $conn)
$that->handleRequest($conn, $request);

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

Expand Down Expand Up @@ -130,6 +130,15 @@ public function handleRequest(ConnectionInterface $conn, Request $request)
'[]'
);

$stream = $conn;
if ($request->hasHeader('Transfer-Encoding')) {
$transferEncodingHeader = $request->getHeader('Transfer-Encoding');
// 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1
if (strtolower(end($transferEncodingHeader)) === 'chunked') {
$stream = new ChunkedDecoder($conn);
}
}

// forward pause/resume calls to underlying connection
$request->on('pause', array($conn, 'pause'));
$request->on('resume', array($conn, 'resume'));
Expand All @@ -141,10 +150,11 @@ public function handleRequest(ConnectionInterface $conn, Request $request)
});

// forward connection events to request
$conn->on('end', function () use ($request) {
$stream->on('end', function () use ($request) {
$request->emit('end');
});
$conn->on('data', function ($data) use ($request) {

$stream->on('data', function ($data) use ($request) {
$request->emit('data', array($data));
});

Expand Down
Loading

0 comments on commit 8dfc3a3

Please sign in to comment.