Passed
Push — master ( 10d9fd...86ff5d )
by Rustam
04:50 queued 02:32
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 58
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 15.7308

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 23
cts 27
cp 0.8519
crap 15.7308
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
class TrustedHostsNetworkResolver implements MiddlewareInterface
64
{
65
    public const IP_HEADER_TYPE_RFC7239 = 'rfc7239';
66
67
    public const DEFAULT_TRUSTED_HEADERS = [
68
        // common:
69
        'x-forwarded-for',
70
        'x-forwarded-host',
71
        'x-forwarded-proto',
72
        'x-forwarded-port',
73
74
        // RFC:
75
        'forward',
76
77
        // Microsoft:
78
        'front-end-https',
79
        'x-rewrite-url',
80
    ];
81
82
    private const DATA_KEY_HOSTS = 'hosts';
83
    private const DATA_KEY_IP_HEADERS = 'ipHeaders';
84
    private const DATA_KEY_HOST_HEADERS = 'hostHeaders';
85
    private const DATA_KEY_URL_HEADERS = 'urlHeaders';
86
    private const DATA_KEY_PROTOCOL_HEADERS = 'protocolHeaders';
87
    private const DATA_KEY_TRUSTED_HEADERS = 'trustedHeaders';
88
    private const DATA_KEY_PORT_HEADERS = 'portHeaders';
89
90
    private array $trustedHosts = [];
91
    private ?string $attributeIps = null;
92
    private ValidatorInterface $validator;
93
94 35
    public function __construct(ValidatorInterface $validator)
95
    {
96 35
        $this->validator = $validator;
97
    }
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
     * @return self
118
     */
119 32
    public function withAddedTrustedHosts(
120
        array $hosts,
121
        // Defining default headers is not secure!
122
        array $ipHeaders = [],
123
        array $protocolHeaders = [],
124
        array $hostHeaders = [],
125
        array $urlHeaders = [],
126
        array $portHeaders = [],
127
        ?array $trustedHeaders = null
128
    ): self {
129 32
        $new = clone $this;
130
131 32
        foreach ($ipHeaders as $ipHeader) {
132 16
            if (is_string($ipHeader)) {
133 5
                continue;
134
            }
135
136 11
            if (!is_array($ipHeader)) {
137 1
                throw new InvalidArgumentException('Type of IP header is not a string and not array.');
138
            }
139
140 10
            if (count($ipHeader) !== 2) {
141 1
                throw new InvalidArgumentException('The IP header array must have exactly 2 elements.');
142
            }
143
144 9
            [$type, $header] = $ipHeader;
145
146 9
            if (!is_string($type)) {
147 1
                throw new InvalidArgumentException('The IP header type is not a string.');
148
            }
149
150 8
            if (!is_string($header)) {
151 1
                throw new InvalidArgumentException('The IP header value is not a string.');
152
            }
153
154 7
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
155 6
                continue;
156
            }
157
158 1
            throw new InvalidArgumentException("Not supported IP header type: $type.");
159
        }
160
161 27
        if ($hosts === []) {
162 8
            throw new InvalidArgumentException('Empty hosts not allowed.');
163
        }
164
165 19
        $trustedHeaders = $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS;
166 19
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
167
168 15
        $this->checkTypeStringOrArray($hosts, 'hosts');
169 12
        $this->checkTypeStringOrArray($trustedHeaders, 'trustedHeaders');
170 12
        $this->checkTypeStringOrArray($hostHeaders, 'hostHeaders');
171 12
        $this->checkTypeStringOrArray($urlHeaders, 'urlHeaders');
172 12
        $this->checkTypeStringOrArray($portHeaders, 'portHeaders');
173
174 12
        foreach ($hosts as $host) {
175 12
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
176
177 12
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
178 1
                throw new InvalidArgumentException("\"$host\" host is not a domain and not an IP address.");
179
            }
180
        }
181
182 11
        $new->trustedHosts[] = [
183
            self::DATA_KEY_HOSTS => $hosts,
184
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
185
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
186
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
187
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
188
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
189
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
190
        ];
191
192 11
        return $new;
193
    }
194
195
    /**
196
     * Returns a new instance without the trusted hosts and related headers.
197
     *
198
     * @return self
199
     */
200 1
    public function withoutTrustedHosts(): self
201
    {
202 1
        $new = clone $this;
203 1
        $new->trustedHosts = [];
204 1
        return $new;
205
    }
206
207
    /**
208
     * Returns a new instance with the specified request's attribute name to which trusted path data is added.
209
     *
210
     * @param string|null $attribute The request attribute name.
211
     *
212
     * @return self
213
     *
214
     * @see getElementsByRfc7239()
215
     */
216 3
    public function withAttributeIps(?string $attribute): self
217
    {
218 3
        if ($attribute === '') {
219 1
            throw new RuntimeException('Attribute should not be empty string.');
220
        }
221
222 2
        $new = clone $this;
223 2
        $new->attributeIps = $attribute;
224 2
        return $new;
225
    }
226
227 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
228
    {
229 12
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
230
231 12
        if ($actualHost === null) {
232
            // Validation is not possible.
233 1
            return $this->handleNotTrusted($request, $handler);
234
        }
235
236 11
        $trustedHostData = null;
237 11
        $trustedHeaders = [];
238 11
        $validator = function (string $value, array $ranges): Result {
239 10
            return $this->validator->validate(
240
                $value,
241 10
                [new Ip(allowSubnet: false, allowNegation: false, ranges: $ranges)]
242
            );
243
        };
244
245 11
        foreach ($this->trustedHosts as $data) {
246
            // collect all trusted headers
247 10
            $trustedHeaders = array_merge($trustedHeaders, $data[self::DATA_KEY_TRUSTED_HEADERS]);
248
249 10
            if ($trustedHostData !== null) {
250
                // trusted hosts already found
251
                continue;
252
            }
253
254 10
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS], $validator)) {
255 8
                $trustedHostData = $data;
256
            }
257
        }
258
259
        /** @psalm-suppress PossiblyNullArgument, PossiblyNullArrayAccess */
260 11
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
261 11
        $request = $this->removeHeaders($request, $untrustedHeaders);
262
263 11
        if ($trustedHostData === null) {
264
            // No trusted host at all.
265 3
            return $this->handleNotTrusted($request, $handler);
266
        }
267
268 8
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
269 8
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
270
271 8
        if ($ipListType === null) {
272 2
            $hostList = $this->getFormattedIpList($hostList);
273 6
        } elseif ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
274 6
            $hostList = $this->getElementsByRfc7239($hostList);
275
        }
276
277 8
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
278 8
        $hostDataList = [];
279
280
        do {
281 8
            $hostData = array_shift($hostList);
282 8
            if (!isset($hostData['ip'])) {
283
                $hostData = $this->reverseObfuscate($hostData, $hostDataList, $hostList, $request);
284
285
                if ($hostData === null) {
286
                    continue;
287
                }
288
289
                if (!isset($hostData['ip'])) {
290
                    break;
291
                }
292
            }
293
294 8
            $ip = $hostData['ip'];
295
296 8
            if (!$this->isValidHost($ip, ['any'], $validator)) {
297
                // invalid IP
298
                break;
299
            }
300
301 8
            $hostDataList[] = $hostData;
302
303 8
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS], $validator)) {
304
                // not trusted host
305 8
                break;
306
            }
307 8
        } while (count($hostList) > 0);
308
309 8
        if ($this->attributeIps !== null) {
310
            $request = $request->withAttribute($this->attributeIps, $hostDataList);
311
        }
312
313 8
        $uri = $request->getUri();
314
        // find HTTP host
315 8
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
316 4
            if (!$request->hasHeader($hostHeader)) {
317
                continue;
318
            }
319
320
            if (
321 4
                $hostHeader === $ipHeader
322 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
323 4
                && isset($hostData['httpHost'])
324
            ) {
325 2
                $uri = $uri->withHost($hostData['httpHost']);
326 2
                break;
327
            }
328
329 2
            $host = $request->getHeaderLine($hostHeader);
330
331 2
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
332 2
                $uri = $uri->withHost($host);
333 2
                break;
334
            }
335
        }
336
337
        // find protocol
338 8
        foreach ($trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS] as $protocolHeader => $protocols) {
339 4
            if (!$request->hasHeader($protocolHeader)) {
340
                continue;
341
            }
342
343
            if (
344 4
                $protocolHeader === $ipHeader
345 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
346 4
                && isset($hostData['protocol'])
347
            ) {
348 4
                $uri = $uri->withScheme($hostData['protocol']);
349 4
                break;
350
            }
351
352 2
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
353
354 2
            foreach ($protocols as $protocol => $acceptedValues) {
355 2
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
356
                    $uri = $uri->withScheme($protocol);
357
                    break 2;
358
                }
359
            }
360
        }
361
362 8
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
363
364 8
        if ($urlParts !== null) {
365 3
            [$path, $query] = $urlParts;
366 3
            $uri = $uri->withPath($path);
367
368 3
            if ($query !== null) {
369 3
                $uri = $uri->withQuery($query);
370
            }
371
        }
372
373
        // find port
374 8
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
375 1
            if (!$request->hasHeader($portHeader)) {
376
                continue;
377
            }
378
379
            if (
380 1
                $portHeader === $ipHeader
381 1
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
382 1
                && isset($hostData['port'])
383 1
                && $this->checkPort((string)$hostData['port'])
384
            ) {
385 1
                $uri = $uri->withPort($hostData['port']);
386 1
                break;
387
            }
388
389
            $port = $request->getHeaderLine($portHeader);
390
391
            if ($this->checkPort($port)) {
392
                $uri = $uri->withPort((int)$port);
393
                break;
394
            }
395
        }
396
397 8
        return $handler->handle(
398
            $request
399 8
                ->withUri($uri)
400 8
                ->withAttribute('requestClientIp', $hostData['ip'] ?? null)
401
        );
402
    }
403
404
    /**
405
     * Validate host by range.
406
     *
407
     * This method can be extendable by overwriting e.g. with reverse DNS verification.
408
     */
409 10
    protected function isValidHost(string $host, array $ranges, Closure $validator): bool
410
    {
411 10
        return $validator($host, $ranges)->isValid();
412
    }
413
414
    /**
415
     * Reverse obfuscating host data
416
     *
417
     * RFC 7239 allows to use obfuscated host data. In this case, either specifying the
418
     * IP address or dropping the proxy endpoint is required to determine validated route.
419
     *
420
     * By default it does not perform any transformation on the data. You can override this method.
421
     *
422
     * @param array $hostData
423
     * @param array $hostDataListValidated
424
     * @param array $hostDataListRemaining
425
     * @param RequestInterface $request
426
     *
427
     * @return array|null reverse obfuscated host data or null.
428
     * In case of null data is discarded and the process continues with the next portion of host data.
429
     * If the return value is an array, it must contain at least the `ip` key.
430
     *
431
     * @see getElementsByRfc7239()
432
     * @link https://tools.ietf.org/html/rfc7239#section-6.2
433
     * @link https://tools.ietf.org/html/rfc7239#section-6.3
434
     */
435
    protected function reverseObfuscate(
436
        array $hostData,
437
        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

437
        /** @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...
438
        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

438
        /** @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...
439
        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

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