Passed
Push — master ( 53961f...e1754f )
by Alexander
02:48 queued 44s
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 58
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 15.6565

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 24
cts 28
cp 0.8571
crap 15.6565
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 InvalidArgumentException;
8
use Psr\Http\Message\RequestInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use RuntimeException;
14
use Yiisoft\Http\HeaderValueHelper;
15
use Yiisoft\NetworkUtilities\IpHelper;
16
use Yiisoft\Validator\Rule\Ip;
17
18
use function array_diff;
19
use function array_pad;
20
use function array_reverse;
21
use function array_shift;
22
use function array_unshift;
23
use function count;
24
use function explode;
25
use function in_array;
26
use function is_array;
27
use function is_callable;
28
use function filter_var;
29
use function is_string;
30
use function preg_match;
31
use function str_replace;
32
use function strpos;
33
use function strtolower;
34
use function trim;
35
36
/**
37
 * Trusted hosts network resolver.
38
 *
39
 * ```php
40
 * (new TrustedHostsNetworkResolver($responseFactory))
41
 * ->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 the constants IP_HEADER_TYPE_ *.
45
 *   // Headers containing multiple sub-elements (eg RFC 7239) must also be listed for other relevant types
46
 *   // (eg. 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
 *   // Trusted headers. It is a good idea to list all relevant headers.
55
 *   ['x-forwarded-for', 'forwarded', ...]
56
 * )
57
 * ->withAddedTrustedHosts(...)
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 ?Ip $ipValidator = null;
91
92
    /**
93
     * Returns a new instance with the added trusted hosts and related headers.
94
     *
95
     * The header lists are evaluated in the order they were specified.
96
     * If you specify multiple headers by type (eg IP headers), you must ensure that the irrelevant header is removed
97
     * eg. web server application, otherwise spoof clients can be use this vulnerability.
98
     *
99
     * @param string[] $hosts List of trusted hosts IP addresses. If {@see isValidHost()} method is extended,
100
     * then can use domain names with reverse DNS resolving eg. yiiframework.com, * .yiiframework.com.
101
     * @param array $ipHeaders List of headers containing IP lists.
102
     * @param array $protocolHeaders List of headers containing protocol. eg.
103
     * ['x-forwarded-for' => ['http' => 'http', 'https' => ['on', 'https']]].
104
     * @param string[] $hostHeaders List of headers containing HTTP host.
105
     * @param string[] $urlHeaders List of headers containing HTTP URL.
106
     * @param string[] $portHeaders List of headers containing port number.
107
     * @param string[]|null $trustedHeaders List of trusted headers. Removed from the request, if in checking process
108
     * are classified as untrusted by hosts.
109
     *
110
     * @return self
111
     */
112 32
    public function withAddedTrustedHosts(
113
        array $hosts,
114
        // Defining default headers is not secure!
115
        array $ipHeaders = [],
116
        array $protocolHeaders = [],
117
        array $hostHeaders = [],
118
        array $urlHeaders = [],
119
        array $portHeaders = [],
120
        ?array $trustedHeaders = null
121
    ): self {
122 32
        $new = clone $this;
123
124 32
        foreach ($ipHeaders as $ipHeader) {
125 16
            if (is_string($ipHeader)) {
126 5
                continue;
127
            }
128
129 11
            if (!is_array($ipHeader)) {
130 1
                throw new InvalidArgumentException('Type of IP header is not a string and not array.');
131
            }
132
133 10
            if (count($ipHeader) !== 2) {
134 1
                throw new InvalidArgumentException('The IP header array must have exactly 2 elements.');
135
            }
136
137 9
            [$type, $header] = $ipHeader;
138
139 9
            if (!is_string($type)) {
140 1
                throw new InvalidArgumentException('The IP header type is not a string.');
141
            }
142
143 8
            if (!is_string($header)) {
144 1
                throw new InvalidArgumentException('The IP header value is not a string.');
145
            }
146
147 7
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
148 6
                continue;
149
            }
150
151 1
            throw new InvalidArgumentException("Not supported IP header type: $type.");
152
        }
153
154 27
        if ($hosts === []) {
155 8
            throw new InvalidArgumentException('Empty hosts not allowed.');
156
        }
157
158 19
        $trustedHeaders = $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS;
159 19
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
160
161 15
        $this->checkTypeStringOrArray($hosts, 'hosts');
162 12
        $this->checkTypeStringOrArray($trustedHeaders, 'trustedHeaders');
163 12
        $this->checkTypeStringOrArray($hostHeaders, 'hostHeaders');
164 12
        $this->checkTypeStringOrArray($urlHeaders, 'urlHeaders');
165 12
        $this->checkTypeStringOrArray($portHeaders, 'portHeaders');
166
167 12
        foreach ($hosts as $host) {
168 12
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
169
170 12
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
171 1
                throw new InvalidArgumentException("\"$host\" host is not a domain and not an IP address.");
172
            }
173
        }
174
175 11
        $new->trustedHosts[] = [
176 11
            self::DATA_KEY_HOSTS => $hosts,
177 11
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
178 11
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
179 11
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
180 11
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
181 11
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
182 11
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
183
        ];
184
185 11
        return $new;
186
    }
187
188
    /**
189
     * Returns a new instance without the trusted hosts and related headers.
190
     *
191
     * @return self
192
     */
193 1
    public function withoutTrustedHosts(): self
194
    {
195 1
        $new = clone $this;
196 1
        $new->trustedHosts = [];
197 1
        return $new;
198
    }
199
200
    /**
201
     * Returns a new instance with the specified request's attribute name to which trusted path data is added.
202
     *
203
     * The list starts with the server and the last item is the client itself.
204
     *
205
     * @param string|null $attribute The request attribute name.
206
     *
207
     * @see getElementsByRfc7239()
208
     *
209
     * @return self
210
     */
211 3
    public function withAttributeIps(?string $attribute): self
212
    {
213 3
        if ($attribute === '') {
214 1
            throw new RuntimeException('Attribute should not be empty string.');
215
        }
216
217 2
        $new = clone $this;
218 2
        $new->attributeIps = $attribute;
219 2
        return $new;
220
    }
221
222
    /**
223
     * Returns a new instance with the specified client IP validator.
224
     *
225
     * @param Ip $ipValidator Client IP validator.
226
     *
227
     * @return self
228
     */
229 1
    public function withIpValidator(Ip $ipValidator): self
230
    {
231 1
        $new = clone $this;
232 1
        $new->ipValidator = $ipValidator;
233 1
        return $new;
234
    }
235
236 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
237
    {
238 12
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
239
240 12
        if ($actualHost === null) {
241
            // Validation is not possible.
242 1
            return $this->handleNotTrusted($request, $handler);
243
        }
244
245 11
        $trustedHostData = null;
246 11
        $trustedHeaders = [];
247 11
        $ipValidator = ($this->ipValidator ?? Ip::rule())->disallowSubnet()->disallowNegation();
248
249 11
        foreach ($this->trustedHosts as $data) {
250
            // collect all trusted headers
251 10
            $trustedHeaders = array_merge($trustedHeaders, $data[self::DATA_KEY_TRUSTED_HEADERS]);
252
253 10
            if ($trustedHostData !== null) {
254
                // trusted hosts already found
255
                continue;
256
            }
257
258 10
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS], $ipValidator)) {
259 8
                $trustedHostData = $data;
260
            }
261
        }
262
263
        /** @psalm-suppress PossiblyNullArgument, PossiblyNullArrayAccess */
264 11
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
265 11
        $request = $this->removeHeaders($request, $untrustedHeaders);
266
267 11
        if ($trustedHostData === null) {
268
            // No trusted host at all.
269 3
            return $this->handleNotTrusted($request, $handler);
270
        }
271
272 8
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
273 8
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
274
275 8
        if ($ipListType === null) {
276 2
            $hostList = $this->getFormattedIpList($hostList);
277 6
        } elseif ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
278 6
            $hostList = $this->getElementsByRfc7239($hostList);
279
        }
280
281 8
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
282 8
        $hostDataList = [];
283
284
        do {
285 8
            $hostData = array_shift($hostList);
286 8
            if (!isset($hostData['ip'])) {
287
                $hostData = $this->reverseObfuscate($hostData, $hostDataList, $hostList, $request);
288
289
                if ($hostData === null) {
290
                    continue;
291
                }
292
293
                if (!isset($hostData['ip'])) {
294
                    break;
295
                }
296
            }
297
298 8
            $ip = $hostData['ip'];
299
300 8
            if (!$this->isValidHost($ip, ['any'], $ipValidator)) {
301
                // invalid IP
302
                break;
303
            }
304
305 8
            $hostDataList[] = $hostData;
306
307 8
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS], $ipValidator)) {
308
                // not trusted host
309 8
                break;
310
            }
311 8
        } while (count($hostList) > 0);
312
313 8
        if ($this->attributeIps !== null) {
314
            $request = $request->withAttribute($this->attributeIps, $hostDataList);
315
        }
316
317 8
        $uri = $request->getUri();
318
        // find HTTP host
319 8
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
320 4
            if (!$request->hasHeader($hostHeader)) {
321
                continue;
322
            }
323
324
            if (
325 4
                $hostHeader === $ipHeader
326 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
327 4
                && isset($hostData['httpHost'])
328
            ) {
329 2
                $uri = $uri->withHost($hostData['httpHost']);
330 2
                break;
331
            }
332
333 2
            $host = $request->getHeaderLine($hostHeader);
334
335 2
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
336 2
                $uri = $uri->withHost($host);
337 2
                break;
338
            }
339
        }
340
341
        // find protocol
342 8
        foreach ($trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS] as $protocolHeader => $protocols) {
343 4
            if (!$request->hasHeader($protocolHeader)) {
344
                continue;
345
            }
346
347
            if (
348 4
                $protocolHeader === $ipHeader
349 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
350 4
                && isset($hostData['protocol'])
351
            ) {
352 4
                $uri = $uri->withScheme($hostData['protocol']);
353 4
                break;
354
            }
355
356 2
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
357
358 2
            foreach ($protocols as $protocol => $acceptedValues) {
359 2
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
360
                    $uri = $uri->withScheme($protocol);
361
                    break 2;
362
                }
363
            }
364
        }
365
366 8
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
367
368 8
        if ($urlParts !== null) {
369 3
            [$path, $query] = $urlParts;
370 3
            $uri = $uri->withPath($path);
371
372 3
            if ($query !== null) {
373 3
                $uri = $uri->withQuery($query);
374
            }
375
        }
376
377
        // find port
378 8
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
379 1
            if (!$request->hasHeader($portHeader)) {
380
                continue;
381
            }
382
383
            if (
384 1
                $portHeader === $ipHeader
385 1
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
386 1
                && isset($hostData['port'])
387 1
                && $this->checkPort((string) $hostData['port'])
388
            ) {
389 1
                $uri = $uri->withPort($hostData['port']);
390 1
                break;
391
            }
392
393
            $port = $request->getHeaderLine($portHeader);
394
395
            if ($this->checkPort($port)) {
396
                $uri = $uri->withPort((int) $port);
397
                break;
398
            }
399
        }
400
401 8
        return $handler->handle($request->withUri($uri)->withAttribute('requestClientIp', $hostData['ip'] ?? null));
402
    }
403
404
    /**
405
     * Validate host by range.
406
     *
407
     * This method can be extendable by overwriting eg. with reverse DNS verification.
408
     */
409 10
    protected function isValidHost(string $host, array $ranges, Ip $validator): bool
410
    {
411 10
        return $validator->ranges($ranges)->validate($host)->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
     * @link https://tools.ietf.org/html/rfc7239
546
     *
547
     * @return array proxy data elements
548
     */
549 6
    private function getElementsByRfc7239(array $forwards): array
550
    {
551 6
        $list = [];
552
553 6
        foreach ($forwards as $forward) {
554 6
            $data = HeaderValueHelper::getParameters($forward);
555
556 6
            if (!isset($data['for'])) {
557
                // Invalid item, the following items will be dropped
558
                break;
559
            }
560
561 6
            $pattern = '/^(?<host>' . IpHelper::IPV4_PATTERN . '|unknown|_[\w\.-]+|[[]'
562 6
                . IpHelper::IPV6_PATTERN . '[]])(?::(?<port>[\w\.-]+))?$/';
563
564 6
            if (preg_match($pattern, $data['for'], $matches) === 0) {
565
                // Invalid item, the following items will be dropped
566
                break;
567
            }
568
569 6
            $ipData = [];
570 6
            $host = $matches['host'];
571 6
            $obfuscatedHost = $host === 'unknown' || strpos($host, '_') === 0;
572
573 6
            if (!$obfuscatedHost) {
574
                // IPv4 & IPv6
575 6
                $ipData['ip'] = strpos($host, '[') === 0 ? trim($host /* IPv6 */, '[]') : $host;
576
            }
577
578 6
            $ipData['host'] = $host;
579
580 6
            if (isset($matches['port'])) {
581 1
                $port = $matches['port'];
582
583 1
                if (!$obfuscatedHost && !$this->checkPort($port)) {
584
                    // Invalid port, the following items will be dropped
585
                    break;
586
                }
587
588 1
                $ipData['port'] = $obfuscatedHost ? $port : (int)$port;
589
            }
590
591
            // copy other properties
592 6
            foreach (['proto' => 'protocol', 'host' => 'httpHost', 'by' => 'by'] as $source => $destination) {
593 6
                if (isset($data[$source])) {
594 4
                    $ipData[$destination] = $data[$source];
595
                }
596
            }
597
598 6
            if (isset($ipData['httpHost']) && filter_var($ipData['httpHost'], FILTER_VALIDATE_DOMAIN) === false) {
599
                // remove not valid HTTP host
600
                unset($ipData['httpHost']);
601
            }
602
603 6
            $list[] = $ipData;
604
        }
605
606 6
        return $list;
607
    }
608
609 8
    private function getUrl(RequestInterface $request, array $urlHeaders): ?array
610
    {
611 8
        foreach ($urlHeaders as $header) {
612 3
            if (!$request->hasHeader($header)) {
613
                continue;
614
            }
615
616 3
            $url = $request->getHeaderLine($header);
617
618 3
            if (strpos($url, '/') === 0) {
619 3
                return array_pad(explode('?', $url, 2), 2, null);
620
            }
621
        }
622
623 5
        return null;
624
    }
625
626 1
    private function checkPort(string $port): bool
627
    {
628 1
        return preg_match('/^\d{1,5}$/', $port) === 1 && (int)$port <= 65535;
629
    }
630
631 15
    private function checkTypeStringOrArray(array $array, string $field): void
632
    {
633 15
        foreach ($array as $item) {
634 15
            if (!is_string($item)) {
635 1
                throw new InvalidArgumentException("$field must be string type");
636
            }
637
638 14
            if (trim($item) === '') {
639 2
                throw new InvalidArgumentException("$field cannot be empty strings");
640
            }
641
        }
642 12
    }
643
}
644