RequestParser   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 231
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 98.91%
Metric Value
wmc 27
lcom 1
cbo 6
dl 0
loc 231
ccs 91
cts 92
cp 0.9891
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 21 2
B onData() 0 34 4
B parseHead() 0 31 6
A parseUri() 0 18 3
A buildRequest() 0 12 1
A isRequestFinished() 0 17 4
A checkBodySize() 0 6 2
A checkHeadSize() 0 6 2
A checkProtocolVersion() 0 6 2
A getBodySize() 0 4 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\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)) {
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...
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
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...
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