Skip to content

Commit 1a1b0aa

Browse files
committed
Add Chunked Decoder class
Fix Endless loop Fix Add chunk size check and chunk extension handling Handle potential test cases Add ChunkedDecoder Tests Handle potential threat Rename variable Added test to add verify single characters can be emitted Fixing remarks Use Mockbuilder
1 parent 070e3f0 commit 1a1b0aa

File tree

2 files changed

+557
-0
lines changed

2 files changed

+557
-0
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+
}

0 commit comments

Comments
 (0)