GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 5cefd1...492078 )
by Anton
04:08
created

RequestHeaderParser::parseRequest()   F

Complexity

Conditions 41
Paths > 20000

Size

Total Lines 160
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 41
eloc 80
c 1
b 0
f 0
nc 47307
nop 3
dl 0
loc 160
rs 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace React\Http\Io;
4
5
use Evenement\EventEmitter;
6
use Psr\Http\Message\ServerRequestInterface;
7
use React\Http\Message\Response;
8
use React\Http\Message\ServerRequest;
9
use React\Socket\ConnectionInterface;
10
use Exception;
11
12
/**
13
 * [Internal] Parses an incoming request header from an input stream
14
 *
15
 * This is used internally to parse the request header from the connection and
16
 * then process the remaining connection as the request body.
17
 *
18
 * @event headers
19
 * @event error
20
 *
21
 * @internal
22
 */
23
class RequestHeaderParser extends EventEmitter
24
{
25
    private $maxSize = 8192;
26
27
    public function handle(ConnectionInterface $conn)
28
    {
29
        $buffer = '';
30
        $maxSize = $this->maxSize;
31
        $that = $this;
32
        $conn->on('data', $fn = function ($data) use (&$buffer, &$fn, $conn, $maxSize, $that) {
33
            // append chunk of data to buffer and look for end of request headers
34
            $buffer .= $data;
35
            $endOfHeader = \strpos($buffer, "\r\n\r\n");
36
37
            // reject request if buffer size is exceeded
38
            if ($endOfHeader > $maxSize || ($endOfHeader === false && isset($buffer[$maxSize]))) {
39
                $conn->removeListener('data', $fn);
40
                $fn = null;
41
42
                $that->emit('error', array(
43
                    new \OverflowException("Maximum header size of {$maxSize} exceeded.", Response::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE),
44
                    $conn
45
                ));
46
                return;
47
            }
48
49
            // ignore incomplete requests
50
            if ($endOfHeader === false) {
51
                return;
52
            }
53
54
            // request headers received => try to parse request
55
            $conn->removeListener('data', $fn);
56
            $fn = null;
57
58
            try {
59
                $request = $that->parseRequest(
60
                    (string)\substr($buffer, 0, $endOfHeader + 2),
61
                    $conn->getRemoteAddress(),
62
                    $conn->getLocalAddress()
63
                );
64
            } catch (Exception $exception) {
65
                $buffer = '';
66
                $that->emit('error', array(
67
                    $exception,
68
                    $conn
69
                ));
70
                return;
71
            }
72
73
            $contentLength = 0;
74
            if ($request->hasHeader('Transfer-Encoding')) {
75
                $contentLength = null;
76
            } elseif ($request->hasHeader('Content-Length')) {
77
                $contentLength = (int)$request->getHeaderLine('Content-Length');
78
            }
79
80
            if ($contentLength === 0) {
81
                // happy path: request body is known to be empty
82
                $stream = new EmptyBodyStream();
83
                $request = $request->withBody($stream);
84
            } else {
85
                // otherwise body is present => delimit using Content-Length or ChunkedDecoder
86
                $stream = new CloseProtectionStream($conn);
87
                if ($contentLength !== null) {
88
                    $stream = new LengthLimitedStream($stream, $contentLength);
89
                } else {
90
                    $stream = new ChunkedDecoder($stream);
91
                }
92
93
                $request = $request->withBody(new HttpBodyStream($stream, $contentLength));
94
            }
95
96
            $bodyBuffer = isset($buffer[$endOfHeader + 4]) ? \substr($buffer, $endOfHeader + 4) : '';
97
            $buffer = '';
98
            $that->emit('headers', array($request, $conn));
99
100
            if ($bodyBuffer !== '') {
101
                $conn->emit('data', array($bodyBuffer));
102
            }
103
104
            // happy path: request body is known to be empty => immediately end stream
105
            if ($contentLength === 0) {
106
                $stream->emit('end');
107
                $stream->close();
108
            }
109
        });
110
    }
111
112
    /**
113
     * @param string $headers buffer string containing request headers only
114
     * @param ?string $remoteSocketUri
115
     * @param ?string $localSocketUri
116
     * @return ServerRequestInterface
117
     * @throws \InvalidArgumentException
118
     * @internal
119
     */
120
    public function parseRequest($headers, $remoteSocketUri, $localSocketUri)
121
    {
122
        // additional, stricter safe-guard for request line
123
        // because request parser doesn't properly cope with invalid ones
124
        $start = array();
125
        if (!\preg_match('#^(?<method>[^ ]+) (?<target>[^ ]+) HTTP/(?<version>\d\.\d)#m', $headers, $start)) {
126
            throw new \InvalidArgumentException('Unable to parse invalid request-line');
127
        }
128
129
        // only support HTTP/1.1 and HTTP/1.0 requests
130
        if ($start['version'] !== '1.1' && $start['version'] !== '1.0') {
131
            throw new \InvalidArgumentException('Received request with invalid protocol version', Response::STATUS_VERSION_NOT_SUPPORTED);
132
        }
133
134
        // match all request header fields into array, thanks to @kelunik for checking the HTTP specs and coming up with this regex
135
        $matches = array();
136
        $n = \preg_match_all('/^([^()<>@,;:\\\"\/\[\]?={}\x01-\x20\x7F]++):[\x20\x09]*+((?:[\x20\x09]*+[\x21-\x7E\x80-\xFF]++)*+)[\x20\x09]*+[\r]?+\n/m', $headers, $matches, \PREG_SET_ORDER);
137
138
        // check number of valid header fields matches number of lines + request line
139
        if (\substr_count($headers, "\n") !== $n + 1) {
140
            throw new \InvalidArgumentException('Unable to parse invalid request header fields');
141
        }
142
143
        // format all header fields into associative array
144
        $host = null;
145
        $fields = array();
146
        foreach ($matches as $match) {
147
            $fields[$match[1]][] = $match[2];
148
149
            // match `Host` request header
150
            if ($host === null && \strtolower($match[1]) === 'host') {
151
                $host = $match[2];
152
            }
153
        }
154
155
        // create new obj implementing ServerRequestInterface by preserving all
156
        // previous properties and restoring original request-target
157
        $serverParams = array(
158
            'REQUEST_TIME' => \time(),
159
            'REQUEST_TIME_FLOAT' => \microtime(true)
160
        );
161
162
        // scheme is `http` unless TLS is used
163
        $localParts = $localSocketUri === null ? array() : \parse_url($localSocketUri);
164
        if (isset($localParts['scheme']) && $localParts['scheme'] === 'tls') {
165
            $scheme = 'https://';
166
            $serverParams['HTTPS'] = 'on';
167
        } else {
168
            $scheme = 'http://';
169
        }
170
171
        // default host if unset comes from local socket address or defaults to localhost
172
        $hasHost = $host !== null;
173
        if ($host === null) {
174
            $host = isset($localParts['host'], $localParts['port']) ? $localParts['host'] . ':' . $localParts['port'] : '127.0.0.1';
175
        }
176
177
        if ($start['method'] === 'OPTIONS' && $start['target'] === '*') {
178
            // support asterisk-form for `OPTIONS *` request line only
179
            $uri = $scheme . $host;
180
        } elseif ($start['method'] === 'CONNECT') {
181
            $parts = \parse_url('tcp://' . $start['target']);
182
183
            // check this is a valid authority-form request-target (host:port)
184
            if (!isset($parts['scheme'], $parts['host'], $parts['port']) || \count($parts) !== 3) {
185
                throw new \InvalidArgumentException('CONNECT method MUST use authority-form request target');
186
            }
187
            $uri = $scheme . $start['target'];
188
        } else {
189
            // support absolute-form or origin-form for proxy requests
190
            if ($start['target'][0] === '/') {
191
                $uri = $scheme . $host . $start['target'];
192
            } else {
193
                // ensure absolute-form request-target contains a valid URI
194
                $parts = \parse_url($start['target']);
195
196
                // make sure value contains valid host component (IP or hostname), but no fragment
197
                if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'http' || isset($parts['fragment'])) {
198
                    throw new \InvalidArgumentException('Invalid absolute-form request-target');
199
                }
200
201
                $uri = $start['target'];
202
            }
203
        }
204
205
        // apply REMOTE_ADDR and REMOTE_PORT if source address is known
206
        // address should always be known, unless this is over Unix domain sockets (UDS)
207
        if ($remoteSocketUri !== null) {
208
            $remoteAddress = \parse_url($remoteSocketUri);
209
            $serverParams['REMOTE_ADDR'] = $remoteAddress['host'];
210
            $serverParams['REMOTE_PORT'] = $remoteAddress['port'];
211
        }
212
213
        // apply SERVER_ADDR and SERVER_PORT if server address is known
214
        // address should always be known, even for Unix domain sockets (UDS)
215
        // but skip UDS as it doesn't have a concept of host/port.
216
        if ($localSocketUri !== null && isset($localParts['host'], $localParts['port'])) {
217
            $serverParams['SERVER_ADDR'] = $localParts['host'];
218
            $serverParams['SERVER_PORT'] = $localParts['port'];
219
        }
220
221
        $request = new ServerRequest(
222
            $start['method'],
223
            $uri,
224
            $fields,
225
            '',
226
            $start['version'],
227
            $serverParams
228
        );
229
230
        // only assign request target if it is not in origin-form (happy path for most normal requests)
231
        if ($start['target'][0] !== '/') {
232
            $request = $request->withRequestTarget($start['target']);
233
        }
234
235
        if ($hasHost) {
236
            // Optional Host request header value MUST be valid (host and optional port)
237
            $parts = \parse_url('http://' . $request->getHeaderLine('Host'));
238
239
            // make sure value contains valid host component (IP or hostname)
240
            if (!$parts || !isset($parts['scheme'], $parts['host'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts 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...
241
                $parts = false;
242
            }
243
244
            // make sure value does not contain any other URI component
245
            if (\is_array($parts)) {
246
                unset($parts['scheme'], $parts['host'], $parts['port']);
247
            }
248
            if ($parts === false || $parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts 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...
249
                throw new \InvalidArgumentException('Invalid Host header value');
250
            }
251
        } elseif (!$hasHost && $start['version'] === '1.1' && $start['method'] !== 'CONNECT') {
0 ignored issues
show
introduced by
The condition $hasHost is always false.
Loading history...
252
            // require Host request header for HTTP/1.1 (except for CONNECT method)
253
            throw new \InvalidArgumentException('Missing required Host request header');
254
        } elseif (!$hasHost) {
255
            // remove default Host request header for HTTP/1.0 when not explicitly given
256
            $request = $request->withoutHeader('Host');
257
        }
258
259
        // ensure message boundaries are valid according to Content-Length and Transfer-Encoding request headers
260
        if ($request->hasHeader('Transfer-Encoding')) {
261
            if (\strtolower($request->getHeaderLine('Transfer-Encoding')) !== 'chunked') {
262
                throw new \InvalidArgumentException('Only chunked-encoding is allowed for Transfer-Encoding', Response::STATUS_NOT_IMPLEMENTED);
263
            }
264
265
            // Transfer-Encoding: chunked and Content-Length header MUST NOT be used at the same time
266
            // as per https://tools.ietf.org/html/rfc7230#section-3.3.3
267
            if ($request->hasHeader('Content-Length')) {
268
                throw new \InvalidArgumentException('Using both `Transfer-Encoding: chunked` and `Content-Length` is not allowed', Response::STATUS_BAD_REQUEST);
269
            }
270
        } elseif ($request->hasHeader('Content-Length')) {
271
            $string = $request->getHeaderLine('Content-Length');
272
273
            if ((string)(int)$string !== $string) {
274
                // Content-Length value is not an integer or not a single integer
275
                throw new \InvalidArgumentException('The value of `Content-Length` is not valid', Response::STATUS_BAD_REQUEST);
276
            }
277
        }
278
279
        return $request;
280
    }
281
}
282