Test Failed
Pull Request — master (#5)
by Dmitriy
02:26
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 58
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 15.8185

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 27
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 58
ccs 22
cts 26
cp 0.8462
crap 15.8185
rs 5.9166

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
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Middleware;
6
7
use Closure;
8
use InvalidArgumentException;
9
use Psr\Http\Message\RequestInterface;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\ServerRequestInterface;
12
use Psr\Http\Server\MiddlewareInterface;
13
use Psr\Http\Server\RequestHandlerInterface;
14
use RuntimeException;
15
use Yiisoft\Http\HeaderValueHelper;
16
use Yiisoft\NetworkUtilities\IpHelper;
17
use Yiisoft\Validator\Rule\Ip\Ip;
18
use Yiisoft\Validator\ValidatorInterface;
19
use function array_diff;
20
use function array_pad;
21
use function array_reverse;
22
use function array_shift;
23
use function array_unshift;
24
use function count;
25
use function explode;
26
use function filter_var;
27
use function in_array;
28
use function is_array;
29
use function is_callable;
30
use function is_string;
31
use function preg_match;
32
use function str_replace;
33
use function strpos;
34
use function strtolower;
35
use function trim;
36
37
/**
38
 * Trusted hosts network resolver.
39
 *
40
 * ```php
41
 * $trustedHostsNetworkResolver->withAddedTrustedHosts(
42
 *   // List of secure hosts including $_SERVER['REMOTE_ADDR'], can specify IPv4, IPv6, domains and aliases {@see Ip}.
43
 *   ['1.1.1.1', '2.2.2.1/3', '2001::/32', 'localhost'].
44
 *   // IP list headers. For advanced handling headers {@see TrustedHostsNetworkResolver::IP_HEADER_TYPE_RFC7239}.
45
 *   // Headers containing multiple sub-elements (e.g. RFC 7239) must also be listed for other relevant types
46
 *   // (e.g. host headers), otherwise they will only be used as an IP list.
47
 *   ['x-forwarded-for', [TrustedHostsNetworkResolver::IP_HEADER_TYPE_RFC7239, 'forwarded']]
48
 *   // Protocol headers with accepted protocols and values. Matching of values is case-insensitive.
49
 *   ['front-end-https' => ['https' => 'on']],
50
 *   // Host headers
51
 *   ['forwarded', 'x-forwarded-for']
52
 *   // URL headers
53
 *   ['x-rewrite-url'],
54
 *   // Port headers
55
 *   ['x-rewrite-port'],
56
 *   // Trusted headers. It is a good idea to list all relevant headers.
57
 *   ['x-forwarded-for', 'forwarded', ...],
58
 * );
59
 * ```
60
 */
61
class TrustedHostsNetworkResolver implements MiddlewareInterface
62
{
63
    public const IP_HEADER_TYPE_RFC7239 = 'rfc7239';
64
65
    public const DEFAULT_TRUSTED_HEADERS = [
66
        // common:
67
        'x-forwarded-for',
68
        'x-forwarded-host',
69
        'x-forwarded-proto',
70
        'x-forwarded-port',
71
72
        // RFC:
73
        'forward',
74
75
        // Microsoft:
76
        'front-end-https',
77
        'x-rewrite-url',
78
    ];
79
80
    private const DATA_KEY_HOSTS = 'hosts';
81
    private const DATA_KEY_IP_HEADERS = 'ipHeaders';
82
    private const DATA_KEY_HOST_HEADERS = 'hostHeaders';
83
    private const DATA_KEY_URL_HEADERS = 'urlHeaders';
84
    private const DATA_KEY_PROTOCOL_HEADERS = 'protocolHeaders';
85
    private const DATA_KEY_TRUSTED_HEADERS = 'trustedHeaders';
86
    private const DATA_KEY_PORT_HEADERS = 'portHeaders';
87
88
    private array $trustedHosts = [];
89
    private ?string $attributeIps = null;
90
    private ValidatorInterface $validator;
91
92
    public function __construct(ValidatorInterface $validator)
93
    {
94
        $this->validator = $validator;
95
    }
96
97
    /**
98
     * Returns a new instance with the added trusted hosts and related headers.
99
     *
100
     * The header lists are evaluated in the order they were specified.
101
     * If you specify multiple headers by type (e.g. IP headers), you must ensure that the irrelevant header is removed
102
     * e.g. web server application, otherwise spoof clients can be use this vulnerability.
103
     *
104
     * @param string[] $hosts List of trusted hosts IP addresses. If {@see isValidHost()} method is extended,
105
     * then can use domain names with reverse DNS resolving e.g. yiiframework.com, * .yiiframework.com.
106
     * @param array $ipHeaders List of headers containing IP lists.
107
     * @param array $protocolHeaders List of headers containing protocol. e.g.
108
     * ['x-forwarded-for' => ['http' => 'http', 'https' => ['on', 'https']]].
109
     * @param string[] $hostHeaders List of headers containing HTTP host.
110
     * @param string[] $urlHeaders List of headers containing HTTP URL.
111
     * @param string[] $portHeaders List of headers containing port number.
112 32
     * @param string[]|null $trustedHeaders List of trusted headers. Removed from the request, if in checking process
113
     * are classified as untrusted by hosts.
114
     *
115
     * @return self
116
     */
117
    public function withAddedTrustedHosts(
118
        array $hosts,
119
        // Defining default headers is not secure!
120
        array $ipHeaders = [],
121
        array $protocolHeaders = [],
122 32
        array $hostHeaders = [],
123
        array $urlHeaders = [],
124 32
        array $portHeaders = [],
125 16
        ?array $trustedHeaders = null
126 5
    ): self {
127
        $new = clone $this;
128
129 11
        foreach ($ipHeaders as $ipHeader) {
130 1
            if (is_string($ipHeader)) {
131
                continue;
132
            }
133 10
134 1
            if (!is_array($ipHeader)) {
135
                throw new InvalidArgumentException('Type of IP header is not a string and not array.');
136
            }
137 9
138
            if (count($ipHeader) !== 2) {
139 9
                throw new InvalidArgumentException('The IP header array must have exactly 2 elements.');
140 1
            }
141
142
            [$type, $header] = $ipHeader;
143 8
144 1
            if (!is_string($type)) {
145
                throw new InvalidArgumentException('The IP header type is not a string.');
146
            }
147 7
148 6
            if (!is_string($header)) {
149
                throw new InvalidArgumentException('The IP header value is not a string.');
150
            }
151 1
152
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
153
                continue;
154 27
            }
155 8
156
            throw new InvalidArgumentException("Not supported IP header type: $type.");
157
        }
158 19
159 19
        if ($hosts === []) {
160
            throw new InvalidArgumentException('Empty hosts not allowed.');
161 15
        }
162 12
163 12
        $trustedHeaders = $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS;
164 12
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
165 12
166
        $this->checkTypeStringOrArray($hosts, 'hosts');
167 12
        $this->checkTypeStringOrArray($trustedHeaders, 'trustedHeaders');
168 12
        $this->checkTypeStringOrArray($hostHeaders, 'hostHeaders');
169
        $this->checkTypeStringOrArray($urlHeaders, 'urlHeaders');
170 12
        $this->checkTypeStringOrArray($portHeaders, 'portHeaders');
171 1
172
        foreach ($hosts as $host) {
173
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
174
175 11
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
176 11
                throw new InvalidArgumentException("\"$host\" host is not a domain and not an IP address.");
177 11
            }
178 11
        }
179 11
180 11
        $new->trustedHosts[] = [
181 11
            self::DATA_KEY_HOSTS => $hosts,
182 11
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
183
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
184
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
185 11
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
186
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
187
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
188
        ];
189
190
        return $new;
191
    }
192
193 1
    /**
194
     * Returns a new instance without the trusted hosts and related headers.
195 1
     *
196 1
     * @return self
197 1
     */
198
    public function withoutTrustedHosts(): self
199
    {
200
        $new = clone $this;
201
        $new->trustedHosts = [];
202
        return $new;
203
    }
204
205
    /**
206
     * Returns a new instance with the specified request's attribute name to which trusted path data is added.
207
     *
208
     * @param string|null $attribute The request attribute name.
209 3
     *
210
     * @see getElementsByRfc7239()
211 3
     *
212 1
     * @return self
213
     */
214
    public function withAttributeIps(?string $attribute): self
215 2
    {
216 2
        if ($attribute === '') {
217 2
            throw new RuntimeException('Attribute should not be empty string.');
218
        }
219
220
        $new = clone $this;
221
        $new->attributeIps = $attribute;
222
        return $new;
223
    }
224
225
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
226
    {
227 1
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
228
229 1
        if ($actualHost === null) {
230 1
            // Validation is not possible.
231 1
            return $this->handleNotTrusted($request, $handler);
232
        }
233
234 12
        $trustedHostData = null;
235
        $trustedHeaders = [];
236 12
        $validator = function ($value, array $ranges) {
237
            return $this->validator->validate(
238 12
                $value,
239
                [new Ip(allowNegation: false, allowSubnet: false, ranges: $ranges)]
240 1
            );
241
        };
242
243 11
        foreach ($this->trustedHosts as $data) {
244 11
            // collect all trusted headers
245 11
            $trustedHeaders = array_merge($trustedHeaders, $data[self::DATA_KEY_TRUSTED_HEADERS]);
246
247 11
            if ($trustedHostData !== null) {
248
                // trusted hosts already found
249 10
                continue;
250
            }
251 10
252
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS], $validator)) {
253
                $trustedHostData = $data;
254
            }
255
        }
256 10
257 8
        /** @psalm-suppress PossiblyNullArgument, PossiblyNullArrayAccess */
258
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
259
        $request = $this->removeHeaders($request, $untrustedHeaders);
260
261
        if ($trustedHostData === null) {
262 11
            // No trusted host at all.
263 11
            return $this->handleNotTrusted($request, $handler);
264
        }
265 11
266
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
267 3
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
268
269
        if ($ipListType === null) {
270 8
            $hostList = $this->getFormattedIpList($hostList);
271 8
        } elseif ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
272
            $hostList = $this->getElementsByRfc7239($hostList);
273 8
        }
274 2
275 6
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
276 6
        $hostDataList = [];
277
278
        do {
279 8
            $hostData = array_shift($hostList);
280 8
            if (!isset($hostData['ip'])) {
281
                $hostData = $this->reverseObfuscate($hostData, $hostDataList, $hostList, $request);
282
283 8
                if ($hostData === null) {
284 8
                    continue;
285
                }
286
287
                if (!isset($hostData['ip'])) {
288
                    break;
289
                }
290
            }
291
292
            $ip = $hostData['ip'];
293
294
            if (!$this->isValidHost($ip, ['any'], $validator)) {
295
                // invalid IP
296 8
                break;
297
            }
298 8
299
            $hostDataList[] = $hostData;
300
301
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS], $validator)) {
302
                // not trusted host
303 8
                break;
304
            }
305 8
        } while (count($hostList) > 0);
306
307 8
        if ($this->attributeIps !== null) {
308
            $request = $request->withAttribute($this->attributeIps, $hostDataList);
309 8
        }
310
311 8
        $uri = $request->getUri();
312
        // find HTTP host
313
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
314
            if (!$request->hasHeader($hostHeader)) {
315 8
                continue;
316
            }
317 8
318 4
            if (
319
                $hostHeader === $ipHeader
320
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
321
                && isset($hostData['httpHost'])
322
            ) {
323 4
                $uri = $uri->withHost($hostData['httpHost']);
324 4
                break;
325 4
            }
326
327 2
            $host = $request->getHeaderLine($hostHeader);
328 2
329
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
330
                $uri = $uri->withHost($host);
331 2
                break;
332
            }
333 2
        }
334 2
335 2
        // find protocol
336
        foreach ($trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS] as $protocolHeader => $protocols) {
337
            if (!$request->hasHeader($protocolHeader)) {
338
                continue;
339
            }
340 8
341 4
            if (
342
                $protocolHeader === $ipHeader
343
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
344
                && isset($hostData['protocol'])
345
            ) {
346 4
                $uri = $uri->withScheme($hostData['protocol']);
347 4
                break;
348 4
            }
349
350 4
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
351 4
352
            foreach ($protocols as $protocol => $acceptedValues) {
353
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
354 2
                    $uri = $uri->withScheme($protocol);
355
                    break 2;
356 2
                }
357 2
            }
358
        }
359
360
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
361
362
        if ($urlParts !== null) {
363
            [$path, $query] = $urlParts;
364 8
            $uri = $uri->withPath($path);
365
366 8
            if ($query !== null) {
367 3
                $uri = $uri->withQuery($query);
368 3
            }
369
        }
370 3
371 3
        // find port
372
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
373
            if (!$request->hasHeader($portHeader)) {
374
                continue;
375
            }
376 8
377 1
            if (
378
                $portHeader === $ipHeader
379
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
380
                && isset($hostData['port'])
381
                && $this->checkPort((string) $hostData['port'])
382 1
            ) {
383 1
                $uri = $uri->withPort($hostData['port']);
384 1
                break;
385 1
            }
386
387 1
            $port = $request->getHeaderLine($portHeader);
388 1
389
            if ($this->checkPort($port)) {
390
                $uri = $uri->withPort((int) $port);
391
                break;
392
            }
393
        }
394
395
        return $handler->handle($request->withUri($uri)->withAttribute('requestClientIp', $hostData['ip'] ?? null));
396
    }
397
398
    /**
399 8
     * Validate host by range.
400
     *
401
     * This method can be extendable by overwriting e.g. with reverse DNS verification.
402
     */
403
    protected function isValidHost(string $host, array $ranges, Closure $validator): bool
404
    {
405
        return $validator($host,$ranges)->isValid();
406
    }
407 10
408
    /**
409 10
     * Reverse obfuscating host data
410
     *
411
     * RFC 7239 allows to use obfuscated host data. In this case, either specifying the
412
     * IP address or dropping the proxy endpoint is required to determine validated route.
413
     *
414
     * By default it does not perform any transformation on the data. You can override this method.
415
     *
416
     * @param array $hostData
417
     * @param array $hostDataListValidated
418
     * @param array $hostDataListRemaining
419
     * @param RequestInterface $request
420
     *
421
     * @return array|null reverse obfuscated host data or null.
422
     * In case of null data is discarded and the process continues with the next portion of host data.
423
     * If the return value is an array, it must contain at least the `ip` key.
424
     *
425
     * @see getElementsByRfc7239()
426
     * @link https://tools.ietf.org/html/rfc7239#section-6.2
427
     * @link https://tools.ietf.org/html/rfc7239#section-6.3
428
     */
429
    protected function reverseObfuscate(
430
        array $hostData,
431
        array $hostDataListValidated,
0 ignored issues
show
Unused Code introduced by
The parameter $hostDataListValidated is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

431
        /** @scrutinizer ignore-unused */ array $hostDataListValidated,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
432
        array $hostDataListRemaining,
0 ignored issues
show
Unused Code introduced by
The parameter $hostDataListRemaining is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

432
        /** @scrutinizer ignore-unused */ array $hostDataListRemaining,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
433
        RequestInterface $request
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

433
        /** @scrutinizer ignore-unused */ RequestInterface $request

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
434
    ): ?array {
435
        return $hostData;
436
    }
437
438
    private function handleNotTrusted(
439
        ServerRequestInterface $request,
440
        RequestHandlerInterface $handler
441
    ): ResponseInterface {
442 4
        if ($this->attributeIps !== null) {
443
            $request = $request->withAttribute($this->attributeIps, null);
444
        }
445
446 4
        return $handler->handle($request->withAttribute('requestClientIp', null));
447 1
    }
448
449
    private function prepareProtocolHeaders(array $protocolHeaders): array
450 4
    {
451
        $output = [];
452
453 19
        foreach ($protocolHeaders as $header => $protocolAndAcceptedValues) {
454
            $header = strtolower($header);
455 19
456
            if (is_callable($protocolAndAcceptedValues)) {
457 19
                $output[$header] = $protocolAndAcceptedValues;
458 8
                continue;
459
            }
460 8
461
            if (!is_array($protocolAndAcceptedValues)) {
462
                throw new RuntimeException('Accepted values is not an array nor callable.');
463
            }
464
465 8
            if ($protocolAndAcceptedValues === []) {
466 1
                throw new RuntimeException('Accepted values cannot be an empty array.');
467
            }
468
469 7
            $output[$header] = [];
470 1
471
            foreach ($protocolAndAcceptedValues as $protocol => $acceptedValues) {
472
                if (!is_string($protocol)) {
473 6
                    throw new RuntimeException('The protocol must be a string.');
474
                }
475 6
476 6
                if ($protocol === '') {
477 1
                    throw new RuntimeException('The protocol cannot be empty.');
478
                }
479
480 5
                $output[$header][$protocol] = array_map('\strtolower', (array)$acceptedValues);
481 1
            }
482
        }
483
484 4
        return $output;
485
    }
486
487
    private function removeHeaders(ServerRequestInterface $request, array $headers): ServerRequestInterface
488 15
    {
489
        foreach ($headers as $header) {
490
            $request = $request->withoutAttribute($header);
491 11
        }
492
493 11
        return $request;
494
    }
495
496
    private function getIpList(RequestInterface $request, array $ipHeaders): array
497 11
    {
498
        foreach ($ipHeaders as $ipHeader) {
499
            $type = null;
500 8
501
            if (is_array($ipHeader)) {
502 8
                $type = array_shift($ipHeader);
503 8
                $ipHeader = array_shift($ipHeader);
504
            }
505 8
506 6
            if ($request->hasHeader($ipHeader)) {
507 6
                return [$type, $ipHeader, $request->getHeader($ipHeader)];
508
            }
509
        }
510 8
511 8
        return [null, null, []];
512
    }
513
514
    /**
515
     * @see getElementsByRfc7239
516
     */
517
    private function getFormattedIpList(array $forwards): array
518
    {
519
        $list = [];
520
521 2
        foreach ($forwards as $ip) {
522
            $list[] = ['ip' => $ip];
523 2
        }
524
525 2
        return $list;
526 2
    }
527
528
    /**
529 2
     * Forwarded elements by RFC7239.
530
     *
531
     * The structure of the elements:
532
     * - `host`: IP or obfuscated hostname or "unknown"
533
     * - `ip`: IP address (only if presented)
534
     * - `by`: used user-agent by proxy (only if presented)
535
     * - `port`: port number received by proxy (only if presented)
536
     * - `protocol`: protocol received by proxy (only if presented)
537
     * - `httpHost`: HTTP host received by proxy (only if presented)
538
     *
539
     * The list starts with the server and the last item is the client itself.
540
     *
541
     * @link https://tools.ietf.org/html/rfc7239
542
     *
543
     * @return array Proxy data elements.
544
     */
545
    private function getElementsByRfc7239(array $forwards): array
546
    {
547
        $list = [];
548
549 6
        foreach ($forwards as $forward) {
550
            $data = HeaderValueHelper::getParameters($forward);
551 6
552
            if (!isset($data['for'])) {
553 6
                // Invalid item, the following items will be dropped
554 6
                break;
555
            }
556 6
557
            $pattern = '/^(?<host>' . IpHelper::IPV4_PATTERN . '|unknown|_[\w\.-]+|[[]'
558
                . IpHelper::IPV6_PATTERN . '[]])(?::(?<port>[\w\.-]+))?$/';
559
560
            if (preg_match($pattern, $data['for'], $matches) === 0) {
561 6
                // Invalid item, the following items will be dropped
562 6
                break;
563
            }
564 6
565
            $ipData = [];
566
            $host = $matches['host'];
567
            $obfuscatedHost = $host === 'unknown' || strpos($host, '_') === 0;
568
569 6
            if (!$obfuscatedHost) {
570 6
                // IPv4 & IPv6
571 6
                $ipData['ip'] = strpos($host, '[') === 0 ? trim($host /* IPv6 */, '[]') : $host;
572
            }
573 6
574
            $ipData['host'] = $host;
575 6
576
            if (isset($matches['port'])) {
577
                $port = $matches['port'];
578 6
579
                if (!$obfuscatedHost && !$this->checkPort($port)) {
580 6
                    // Invalid port, the following items will be dropped
581 1
                    break;
582
                }
583 1
584
                $ipData['port'] = $obfuscatedHost ? $port : (int)$port;
585
            }
586
587
            // copy other properties
588 1
            foreach (['proto' => 'protocol', 'host' => 'httpHost', 'by' => 'by'] as $source => $destination) {
589
                if (isset($data[$source])) {
590
                    $ipData[$destination] = $data[$source];
591
                }
592 6
            }
593 6
594 4
            if (isset($ipData['httpHost']) && filter_var($ipData['httpHost'], FILTER_VALIDATE_DOMAIN) === false) {
595
                // remove not valid HTTP host
596
                unset($ipData['httpHost']);
597
            }
598 6
599
            $list[] = $ipData;
600
        }
601
602
        return $list;
603 6
    }
604
605
    private function getUrl(RequestInterface $request, array $urlHeaders): ?array
606 6
    {
607
        foreach ($urlHeaders as $header) {
608
            if (!$request->hasHeader($header)) {
609 8
                continue;
610
            }
611 8
612 3
            $url = $request->getHeaderLine($header);
613
614
            if (strpos($url, '/') === 0) {
615
                return array_pad(explode('?', $url, 2), 2, null);
616 3
            }
617
        }
618 3
619 3
        return null;
620
    }
621
622
    private function checkPort(string $port): bool
623 5
    {
624
        return preg_match('/^\d{1,5}$/', $port) === 1 && (int)$port <= 65535;
625
    }
626 1
627
    private function checkTypeStringOrArray(array $array, string $field): void
628 1
    {
629
        foreach ($array as $item) {
630
            if (!is_string($item)) {
631 15
                throw new InvalidArgumentException("$field must be string type");
632
            }
633 15
634 15
            if (trim($item) === '') {
635 1
                throw new InvalidArgumentException("$field cannot be empty strings");
636
            }
637
        }
638 14
    }
639
}
640