Skip to content

Commit 8dfc3a3

Browse files
authored
Merge pull request #116 from legionth/chunk-encoding
Decode chunked transfer encoding for incoming requests
2 parents 61a69d7 + 61d7b69 commit 8dfc3a3

File tree

5 files changed

+757
-3
lines changed

5 files changed

+757
-3
lines changed

src/ChunkedDecoder.php

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
<?php
2+
namespace React\Http;
3+
4+
use Evenement\EventEmitter;
5+
use React\Stream\ReadableStreamInterface;
6+
use React\Stream\WritableStreamInterface;
7+
use React\Stream\Util;
8+
use Exception;
9+
10+
/** @internal */
11+
class ChunkedDecoder extends EventEmitter implements ReadableStreamInterface
12+
{
13+
const CRLF = "\r\n";
14+
const MAX_CHUNK_HEADER_SIZE = 1024;
15+
16+
private $closed = false;
17+
private $input;
18+
private $buffer = '';
19+
private $chunkSize = 0;
20+
private $transferredSize = 0;
21+
private $headerCompleted = false;
22+
23+
public function __construct(ReadableStreamInterface $input)
24+
{
25+
$this->input = $input;
26+
27+
$this->input->on('data', array($this, 'handleData'));
28+
$this->input->on('end', array($this, 'handleEnd'));
29+
$this->input->on('error', array($this, 'handleError'));
30+
$this->input->on('close', array($this, 'close'));
31+
}
32+
33+
public function isReadable()
34+
{
35+
return !$this->closed && $this->input->isReadable();
36+
}
37+
38+
public function pause()
39+
{
40+
$this->input->pause();
41+
}
42+
43+
public function resume()
44+
{
45+
$this->input->resume();
46+
}
47+
48+
public function pipe(WritableStreamInterface $dest, array $options = array())
49+
{
50+
Util::pipe($this, $dest, $options);
51+
52+
return $dest;
53+
}
54+
55+
public function close()
56+
{
57+
if ($this->closed) {
58+
return;
59+
}
60+
61+
$this->buffer = '';
62+
63+
$this->closed = true;
64+
65+
$this->input->close();
66+
67+
$this->emit('close');
68+
$this->removeAllListeners();
69+
}
70+
71+
/** @internal */
72+
public function handleEnd()
73+
{
74+
if (!$this->closed) {
75+
$this->handleError(new \Exception('Unexpected end event'));
76+
}
77+
}
78+
79+
/** @internal */
80+
public function handleError(\Exception $e)
81+
{
82+
$this->emit('error', array($e));
83+
$this->close();
84+
}
85+
86+
/** @internal */
87+
public function handleData($data)
88+
{
89+
$this->buffer .= $data;
90+
91+
while ($this->buffer !== '') {
92+
if (!$this->headerCompleted) {
93+
$positionCrlf = strpos($this->buffer, static::CRLF);
94+
95+
if ($positionCrlf === false) {
96+
// Header shouldn't be bigger than 1024 bytes
97+
if (isset($this->buffer[static::MAX_CHUNK_HEADER_SIZE])) {
98+
$this->handleError(new \Exception('Chunk header size inclusive extension bigger than' . static::MAX_CHUNK_HEADER_SIZE. ' bytes'));
99+
}
100+
return;
101+
}
102+
103+
$header = strtolower((string)substr($this->buffer, 0, $positionCrlf));
104+
$hexValue = $header;
105+
106+
if (strpos($header, ';') !== false) {
107+
$array = explode(';', $header);
108+
$hexValue = $array[0];
109+
}
110+
111+
$this->chunkSize = hexdec($hexValue);
112+
if (dechex($this->chunkSize) !== $hexValue) {
113+
$this->handleError(new \Exception($hexValue . ' is not a valid hexadecimal number'));
114+
return;
115+
}
116+
117+
$this->buffer = (string)substr($this->buffer, $positionCrlf + 2);
118+
$this->headerCompleted = true;
119+
if ($this->buffer === '') {
120+
return;
121+
}
122+
}
123+
124+
$chunk = (string)substr($this->buffer, 0, $this->chunkSize - $this->transferredSize);
125+
126+
if ($chunk !== '') {
127+
$this->transferredSize += strlen($chunk);
128+
$this->emit('data', array($chunk));
129+
$this->buffer = (string)substr($this->buffer, strlen($chunk));
130+
}
131+
132+
$positionCrlf = strpos($this->buffer, static::CRLF);
133+
134+
if ($positionCrlf === 0) {
135+
if ($this->chunkSize === 0) {
136+
$this->emit('end');
137+
$this->close();
138+
return;
139+
}
140+
$this->chunkSize = 0;
141+
$this->headerCompleted = false;
142+
$this->transferredSize = 0;
143+
$this->buffer = (string)substr($this->buffer, 2);
144+
}
145+
146+
if ($positionCrlf !== 0 && $this->chunkSize === $this->transferredSize && strlen($this->buffer) > 2) {
147+
// the first 2 characters are not CLRF, send error event
148+
$this->handleError(new \Exception('Chunk does not end with a CLRF'));
149+
return;
150+
}
151+
152+
if ($positionCrlf !== 0 && strlen($this->buffer) < 2) {
153+
// No CLRF found, wait for additional data which could be a CLRF
154+
return;
155+
}
156+
}
157+
}
158+
}

src/Server.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public function handleConnection(ConnectionInterface $conn)
9090
$that->handleRequest($conn, $request);
9191

9292
if ($bodyBuffer !== '') {
93-
$request->emit('data', array($bodyBuffer));
93+
$conn->emit('data', array($bodyBuffer));
9494
}
9595
});
9696

@@ -130,6 +130,15 @@ public function handleRequest(ConnectionInterface $conn, Request $request)
130130
'[]'
131131
);
132132

133+
$stream = $conn;
134+
if ($request->hasHeader('Transfer-Encoding')) {
135+
$transferEncodingHeader = $request->getHeader('Transfer-Encoding');
136+
// 'chunked' must always be the final value of 'Transfer-Encoding' according to: https://tools.ietf.org/html/rfc7230#section-3.3.1
137+
if (strtolower(end($transferEncodingHeader)) === 'chunked') {
138+
$stream = new ChunkedDecoder($conn);
139+
}
140+
}
141+
133142
// forward pause/resume calls to underlying connection
134143
$request->on('pause', array($conn, 'pause'));
135144
$request->on('resume', array($conn, 'resume'));
@@ -141,10 +150,11 @@ public function handleRequest(ConnectionInterface $conn, Request $request)
141150
});
142151

143152
// forward connection events to request
144-
$conn->on('end', function () use ($request) {
153+
$stream->on('end', function () use ($request) {
145154
$request->emit('end');
146155
});
147-
$conn->on('data', function ($data) use ($request) {
156+
157+
$stream->on('data', function ($data) use ($request) {
148158
$request->emit('data', array($data));
149159
});
150160

0 commit comments

Comments
 (0)