Completed
Push — master ( 9fe7e8...7331b7 )
by Aurimas
12:47
created

RequestParser::onData()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 32
Code Lines 18

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 32
rs 8.5806
cc 4
eloc 18
nc 6
nop 1
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\Http\Parser\CookieParamParser;
12
use Thruster\Component\Http\Parser\JsonBodyParser;
13
use Thruster\Component\Http\Parser\MultiPartBodyParser;
14
use Thruster\Component\Http\Parser\ParserInterface;
15
use Thruster\Component\Http\Parser\QueryParamParser;
16
use Thruster\Component\Http\Parser\URLEncodedBodyParser;
17
use Thruster\Component\Stream\StreamInterface;
18
19
/**
20
 * Class RequestParser
21
 *
22
 * @package Thruster\Component\Http
23
 * @author  Aurimas Niekis <[email protected]>
24
 */
25
class RequestParser implements EventEmitterInterface
26
{
27
    use EventEmitterTrait;
28
29
    const HEAD_SEPARATOR = "\r\n\r\n";
30
31
    /**
32
     * @var array
33
     */
34
    protected $options;
35
36
    /**
37
     * @var ParserInterface[]
38
     */
39
    protected $parsers;
40
41
    /**
42
     * @var bool
43
     */
44
    protected $receivedHead;
45
46
    /**
47
     * @var resource
48
     */
49
    protected $head;
50
51
    /**
52
     * @var string
53
     */
54
    protected $body;
55
56
    /**
57
     * @var string
58
     */
59
    protected $httpMethod;
60
61
    /**
62
     * @var string
63
     */
64
    protected $protocolVersion;
65
66
    /**
67
     * @var array
68
     */
69
    protected $headers;
70
71
    /**
72
     * @var string
73
     */
74
    protected $uri;
75
76
    public function __construct(array $options = [])
77
    {
78
        $options += [
79
            'max_head_size' => 8190,
80
            'max_request_line' => 8190,
81
            'max_body_size' => 10 * 1024 * 1024,
82
            'memory_limit' => 1 * 1024 * 1024,
83
            'supported_protocol_versions' => ['1.0' => true, '1.1' => true]
84
        ];
85
86
        $this->parsers = [
87
            new QueryParamParser(),
88
            new CookieParamParser(),
89
            new URLEncodedBodyParser(),
90
            new JsonBodyParser(),
91
            new MultiPartBodyParser()
92
        ];
93
94
        $this->options = $options;
95
        $this->receivedHead = false;
96
        $this->head = '';
0 ignored issues
show
Documentation Bug introduced by
It seems like '' of type string is incompatible with the declared type resource of property $head.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
97
        $this->headers = [];
98
99
        $this->body = fopen('php://temp/maxmemory:' . $options['memory_limit'], 'r+b');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen('php://temp/maxmem...'memory_limit'], 'r+b') of type resource is incompatible with the declared type string of property $body.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
100
101
        if (false === $this->body) {
102
            throw new \RuntimeException('Could not open buffer for body');
103
        }
104
    }
105
106
    /**
107
     * @return ParserInterface[]
108
     */
109
    public function getParsers()
110
    {
111
        return $this->parsers;
112
    }
113
114
    /**
115
     * @param ParserInterface $parser
116
     *
117
     * @return $this
118
     */
119
    public function addParser(ParserInterface $parser) : self
120
    {
121
        $this->parsers[] = $parser;
122
123
        return $this;
124
    }
125
126
    /**
127
     * @param $data
128
     *
129
     * @throws BadRequestException
130
     * @throws RequestEntityTooLargeException
131
     * @throws RequestURITooLongException
132
     */
133
    public function onData($data)
134
    {
135
        if ($this->receivedHead) {
136
            fwrite($this->body, $data);
137
138
            $this->checkBodySize();
139
        } else {
140
            $this->head .= $data;
141
142
            if (false !== strpos($this->head, self::HEAD_SEPARATOR)) {
143
                list($head, $body) = explode(self::HEAD_SEPARATOR, $this->head, 2);
144
145
                fwrite($this->body, $body);
146
                $this->head = $head;
147
148
                $this->checkHeadSize();
149
150
                $this->parseHead();
151
152
                $this->checkProtocolVersion();
153
154
                $this->parseUri();
155
156
                $this->checkBodySize();
157
                $this->receivedHead = true;
158
            }
159
        }
160
161
        if ($this->isRequestFinished()) {
162
            $this->emit('request', [$this->buildRequest()]);
163
        }
164
    }
165
166
    /**
167
     * @throws BadRequestException
168
     * @throws RequestURITooLongException
169
     */
170
    protected function parseHead()
171
    {
172
        $parsedRequestLine = false;
173
        foreach (explode("\n", str_replace(["\r\n", "\n\r", "\r"], "\n", $this->head)) as $line) {
174
            if (false === $parsedRequestLine) {
175
                if (strlen($line) > $this->options['max_request_line']) {
176
                    throw new RequestURITooLongException();
177
                }
178
179
                if (false == preg_match('/^[a-zA-Z]+\s+([a-zA-Z]+:\/\/|\/).*/', $line, $matches)) {
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('/^[a-zA-Z]+\...).*/', $line, $matches) of type integer to the boolean false. If you are specifically checking for 0, consider using something more explicit like === 0 instead.
Loading history...
180
                    throw new BadRequestException();
181
                }
182
183
                $parts = explode(' ', $line, 3);
184
                $this->httpMethod = $parts[0];
185
                $this->protocolVersion = explode('/', $parts[2] ?? 'HTTP/1.1')[1];
186
                $this->uri = $parts[1];
187
188
                $parsedRequestLine = true;
189
                continue;
190
            }
191
192
            if (false !== strpos($line, ':')) {
193
                $parts = explode(':', $line, 2);
194
                $key = trim($parts[0]);
195
                $value = trim($parts[1] ?? '');
196
197
                $this->headers[$key][] = $value;
198
            }
199
        }
200
    }
201
202
    protected function parseUri()
203
    {
204
        $hostKey = array_filter(
205
            array_keys($this->headers),
206
            function ($key) {
207
                return 'host' === strtolower($key);
208
            }
209
        );
210
211
        if (!$hostKey) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hostKey of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

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.

Loading history...
212
            return;
213
        }
214
215
        $host = $this->headers[reset($hostKey)][0];
216
        $scheme = ':443' === substr($host, -4) ? 'https' : 'http';
217
218
        $this->uri = $scheme . '://' . $host . '/' . ltrim($this->uri, '/');
219
    }
220
221
    protected function buildRequest()
222
    {
223
        rewind($this->body);
224
225
        $request = new ServerRequest(
226
            [],
227
            [],
228
            $this->uri,
229
            $this->httpMethod,
230
            $this->body,
231
            $this->headers,
232
            [],
233
            [],
234
            null,
235
            $this->protocolVersion
236
        );
237
238
        foreach ($this->parsers as $parser) {
239
            $request = $parser->parse($request);
240
        }
241
242
        return $request;
243
    }
244
245
    protected function isRequestFinished() : bool
246
    {
247
        if (false === $this->receivedHead) {
248
            return false;
249
        }
250
251
        if (false === isset($this->headers['Content-Length'])) {
252
            return true;
253
        }
254
255
        $contentLength = max($this->headers['Content-Length']);
256
        if ($contentLength <= $this->getBodySize()) {
257
            return true;
258
        }
259
260
        return false;
261
    }
262
263
    /**
264
     * @throws RequestEntityTooLargeException
265
     */
266
    protected function checkBodySize()
267
    {
268
        if ($this->getBodySize() > $this->options['max_body_size']) {
269
            throw new RequestEntityTooLargeException();
270
        }
271
    }
272
273
    /**
274
     * @throws RequestEntityTooLargeException
275
     */
276
    protected function checkHeadSize()
277
    {
278
        if (strlen($this->head) > $this->options['max_head_size']) {
279
            throw new RequestEntityTooLargeException();
280
        }
281
    }
282
283
    /**
284
     * @throws RequestHTTPVersionNotSupported
285
     */
286
    protected function checkProtocolVersion()
287
    {
288
        if (false === isset($this->options['supported_protocol_versions'][$this->protocolVersion])) {
289
            throw new RequestHTTPVersionNotSupported();
290
        }
291
    }
292
293
    protected function getBodySize() : int
294
    {
295
        return fstat($this->body)['size'];
296
    }
297
}
298