This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | namespace Thruster\Component\Http; |
||
4 | |||
5 | use Thruster\Component\EventEmitter\EventEmitterInterface; |
||
6 | use Thruster\Component\EventEmitter\EventEmitterTrait; |
||
7 | use Thruster\Component\Http\Exception\BadRequestException; |
||
8 | use Thruster\Component\Http\Exception\RequestEntityTooLargeException; |
||
9 | use Thruster\Component\Http\Exception\RequestHTTPVersionNotSupported; |
||
10 | use Thruster\Component\Http\Exception\RequestURITooLongException; |
||
11 | use Thruster\Component\HttpMessage\ServerRequest; |
||
12 | |||
13 | /** |
||
14 | * Class RequestParser |
||
15 | * |
||
16 | * @package Thruster\Component\Http |
||
17 | * @author Aurimas Niekis <[email protected]> |
||
18 | */ |
||
19 | class RequestParser implements EventEmitterInterface |
||
20 | { |
||
21 | use EventEmitterTrait; |
||
22 | |||
23 | const HEAD_SEPARATOR = "\r\n\r\n"; |
||
24 | |||
25 | /** |
||
26 | * @var array |
||
27 | */ |
||
28 | protected $options; |
||
29 | |||
30 | /** |
||
31 | * @var bool |
||
32 | */ |
||
33 | protected $receivedHead; |
||
34 | |||
35 | /** |
||
36 | * @var string |
||
37 | */ |
||
38 | protected $head; |
||
39 | |||
40 | /** |
||
41 | * @var resource |
||
42 | */ |
||
43 | protected $body; |
||
44 | |||
45 | /** |
||
46 | * @var string |
||
47 | */ |
||
48 | protected $httpMethod; |
||
49 | |||
50 | /** |
||
51 | * @var string |
||
52 | */ |
||
53 | protected $protocolVersion; |
||
54 | |||
55 | /** |
||
56 | * @var array |
||
57 | */ |
||
58 | protected $headers; |
||
59 | |||
60 | /** |
||
61 | * @var string |
||
62 | */ |
||
63 | protected $uri; |
||
64 | |||
65 | 9 | public function __construct(array $options = []) |
|
66 | { |
||
67 | $options += [ |
||
68 | 9 | 'max_head_size' => 8190, |
|
69 | 'max_request_line' => 8190, |
||
70 | 'max_body_size' => 10 * 1024 * 1024, |
||
71 | 'memory_limit' => 1 * 1024 * 1024, |
||
72 | 'supported_protocol_versions' => ['1.0' => true, '1.1' => true] |
||
73 | ]; |
||
74 | |||
75 | 9 | $this->options = $options; |
|
76 | 9 | $this->receivedHead = false; |
|
77 | 9 | $this->head = ''; |
|
78 | 9 | $this->headers = []; |
|
79 | |||
80 | 9 | $this->body = fopen('php://temp/maxmemory:' . $options['memory_limit'], 'r+b'); |
|
81 | |||
82 | 9 | if (false === $this->body) { |
|
83 | throw new \RuntimeException('Could not open buffer for body'); |
||
84 | } |
||
85 | 9 | } |
|
86 | |||
87 | /** |
||
88 | * @param $data |
||
89 | |||
90 | * @throws BadRequestException |
||
91 | * @throws RequestEntityTooLargeException |
||
92 | * @throws RequestURITooLongException |
||
93 | */ |
||
94 | 9 | public function onData($data) |
|
95 | { |
||
96 | 9 | if ($this->receivedHead) { |
|
97 | 1 | fwrite($this->body, $data); |
|
98 | |||
99 | 1 | $this->checkBodySize(); |
|
100 | } else { |
||
101 | 9 | $this->head .= $data; |
|
102 | |||
103 | 9 | if (false !== strpos($this->head, self::HEAD_SEPARATOR)) { |
|
104 | 9 | list($head, $body) = explode(self::HEAD_SEPARATOR, $this->head, 2); |
|
105 | |||
106 | 9 | fwrite($this->body, $body); |
|
107 | 9 | $this->head = $head; |
|
108 | |||
109 | 9 | $this->checkHeadSize(); |
|
110 | |||
111 | 8 | $this->parseHead(); |
|
112 | |||
113 | 6 | $this->checkProtocolVersion(); |
|
114 | |||
115 | 5 | $this->parseUri(); |
|
116 | |||
117 | 5 | $this->checkBodySize(); |
|
118 | 4 | $this->receivedHead = true; |
|
119 | |||
120 | 4 | $this->emit('received_head', [$this->headers, $this->httpMethod, $this->uri, $this->protocolVersion]); |
|
121 | } |
||
122 | } |
||
123 | |||
124 | 4 | if ($this->isRequestFinished()) { |
|
125 | 4 | $this->emit('request', [$this->buildRequest()]); |
|
126 | } |
||
127 | 4 | } |
|
128 | |||
129 | /** |
||
130 | * @throws BadRequestException |
||
131 | * @throws RequestURITooLongException |
||
132 | */ |
||
133 | 8 | protected function parseHead() |
|
134 | { |
||
135 | 8 | $parsedRequestLine = false; |
|
136 | 8 | foreach (explode("\n", str_replace(["\r\n", "\n\r", "\r"], "\n", $this->head)) as $line) { |
|
137 | 8 | if (false === $parsedRequestLine) { |
|
138 | 8 | if (strlen($line) > $this->options['max_request_line']) { |
|
139 | 1 | throw new RequestURITooLongException(); |
|
140 | } |
||
141 | |||
142 | 7 | if (false == preg_match('/^[a-zA-Z]+\s+([a-zA-Z]+:\/\/|\/).*/', $line, $matches)) { |
|
143 | 1 | throw new BadRequestException(); |
|
144 | } |
||
145 | |||
146 | 6 | $parts = explode(' ', $line, 3); |
|
147 | 6 | $this->httpMethod = $parts[0]; |
|
148 | 6 | $this->protocolVersion = explode('/', $parts[2] ?? 'HTTP/1.1')[1]; |
|
149 | 6 | $this->uri = $parts[1]; |
|
150 | |||
151 | 6 | $parsedRequestLine = true; |
|
152 | 6 | continue; |
|
153 | } |
||
154 | |||
155 | 6 | if (false !== strpos($line, ':')) { |
|
156 | 6 | $parts = explode(':', $line, 2); |
|
157 | 6 | $key = trim($parts[0]); |
|
158 | 6 | $value = trim($parts[1] ?? ''); |
|
159 | |||
160 | 6 | $this->headers[$key][] = $value; |
|
161 | } |
||
162 | } |
||
163 | 6 | } |
|
164 | |||
165 | 5 | protected function parseUri() |
|
166 | { |
||
167 | 5 | $hostKey = array_filter( |
|
168 | 5 | array_keys($this->headers), |
|
169 | 5 | function ($key) { |
|
170 | 5 | return 'host' === strtolower($key); |
|
171 | 5 | } |
|
172 | ); |
||
173 | |||
174 | 5 | if (!$hostKey) { |
|
0 ignored issues
–
show
|
|||
175 | 1 | return; |
|
176 | } |
||
177 | |||
178 | 4 | $host = $this->headers[reset($hostKey)][0]; |
|
179 | 4 | $scheme = ':443' === substr($host, -4) ? 'https' : 'http'; |
|
180 | |||
181 | 4 | $this->uri = $scheme . '://' . $host . '/' . ltrim($this->uri, '/'); |
|
182 | 4 | } |
|
183 | |||
184 | 4 | protected function buildRequest() |
|
185 | { |
||
186 | 4 | rewind($this->body); |
|
187 | |||
188 | 4 | return new ServerRequest( |
|
189 | 4 | $this->httpMethod, |
|
190 | 4 | $this->uri, |
|
191 | 4 | $this->headers, |
|
192 | 4 | $this->body, |
|
193 | 4 | $this->protocolVersion |
|
194 | ); |
||
195 | } |
||
196 | |||
197 | 4 | protected function isRequestFinished() : bool |
|
198 | { |
||
199 | 4 | if (false === $this->receivedHead) { |
|
200 | 4 | return false; |
|
201 | } |
||
202 | |||
203 | 4 | if (false === isset($this->headers['Content-Length'])) { |
|
204 | 3 | return true; |
|
205 | } |
||
206 | |||
207 | 1 | $contentLength = max($this->headers['Content-Length']); |
|
208 | 1 | if ($contentLength <= $this->getBodySize()) { |
|
209 | 1 | return true; |
|
210 | } |
||
211 | |||
212 | 1 | return false; |
|
213 | } |
||
214 | |||
215 | /** |
||
216 | * @throws RequestEntityTooLargeException |
||
217 | */ |
||
218 | 5 | protected function checkBodySize() |
|
219 | { |
||
220 | 5 | if ($this->getBodySize() > $this->options['max_body_size']) { |
|
221 | 1 | throw new RequestEntityTooLargeException(); |
|
222 | } |
||
223 | 4 | } |
|
224 | |||
225 | /** |
||
226 | * @throws RequestEntityTooLargeException |
||
227 | */ |
||
228 | 9 | protected function checkHeadSize() |
|
229 | { |
||
230 | 9 | if (strlen($this->head) > $this->options['max_head_size']) { |
|
231 | 1 | throw new RequestEntityTooLargeException(); |
|
232 | } |
||
233 | 8 | } |
|
234 | |||
235 | /** |
||
236 | * @throws RequestHTTPVersionNotSupported |
||
237 | */ |
||
238 | 6 | protected function checkProtocolVersion() |
|
239 | { |
||
240 | 6 | if (false === isset($this->options['supported_protocol_versions'][$this->protocolVersion])) { |
|
241 | 1 | throw new RequestHTTPVersionNotSupported(); |
|
242 | } |
||
243 | 5 | } |
|
244 | |||
245 | 5 | protected function getBodySize() : int |
|
246 | { |
||
247 | 5 | return fstat($this->body)['size']; |
|
248 | } |
||
249 | } |
||
250 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.