Passed
Pull Request — master (#42)
by Rustam
11:52
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 58
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 15.2764

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

429
        /** @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...
430
        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

430
        /** @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...
431
        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

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