Passed
Pull Request — master (#5)
by Dmitriy
02:37
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\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 35
    public function __construct(ValidatorInterface $validator)
93
    {
94 35
        $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
     * @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 32
    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 32
        $new = clone $this;
128
129 32
        foreach ($ipHeaders as $ipHeader) {
130 16
            if (is_string($ipHeader)) {
131 5
                continue;
132
            }
133
134 11
            if (!is_array($ipHeader)) {
135 1
                throw new InvalidArgumentException('Type of IP header is not a string and not array.');
136
            }
137
138 10
            if (count($ipHeader) !== 2) {
139 1
                throw new InvalidArgumentException('The IP header array must have exactly 2 elements.');
140
            }
141
142 9
            [$type, $header] = $ipHeader;
143
144 9
            if (!is_string($type)) {
145 1
                throw new InvalidArgumentException('The IP header type is not a string.');
146
            }
147
148 8
            if (!is_string($header)) {
149 1
                throw new InvalidArgumentException('The IP header value is not a string.');
150
            }
151
152 7
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
153 6
                continue;
154
            }
155
156 1
            throw new InvalidArgumentException("Not supported IP header type: $type.");
157
        }
158
159 27
        if ($hosts === []) {
160 8
            throw new InvalidArgumentException('Empty hosts not allowed.');
161
        }
162
163 19
        $trustedHeaders = $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS;
164 19
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
165
166 15
        $this->checkTypeStringOrArray($hosts, 'hosts');
167 12
        $this->checkTypeStringOrArray($trustedHeaders, 'trustedHeaders');
168 12
        $this->checkTypeStringOrArray($hostHeaders, 'hostHeaders');
169 12
        $this->checkTypeStringOrArray($urlHeaders, 'urlHeaders');
170 12
        $this->checkTypeStringOrArray($portHeaders, 'portHeaders');
171
172 12
        foreach ($hosts as $host) {
173 12
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
174
175 12
            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 11
        $new->trustedHosts[] = [
181
            self::DATA_KEY_HOSTS => $hosts,
182
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
183
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
184
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
185
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
186
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
187
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
188
        ];
189
190 11
        return $new;
191
    }
192
193
    /**
194
     * Returns a new instance without the trusted hosts and related headers.
195
     *
196
     * @return self
197
     */
198 1
    public function withoutTrustedHosts(): self
199
    {
200 1
        $new = clone $this;
201 1
        $new->trustedHosts = [];
202 1
        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
     *
210
     * @see getElementsByRfc7239()
211
     *
212
     * @return self
213
     */
214 3
    public function withAttributeIps(?string $attribute): self
215
    {
216 3
        if ($attribute === '') {
217 1
            throw new RuntimeException('Attribute should not be empty string.');
218
        }
219
220 2
        $new = clone $this;
221 2
        $new->attributeIps = $attribute;
222 2
        return $new;
223
    }
224
225 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
226
    {
227 12
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
228
229 12
        if ($actualHost === null) {
230
            // Validation is not possible.
231 1
            return $this->handleNotTrusted($request, $handler);
232
        }
233
234 11
        $trustedHostData = null;
235 11
        $trustedHeaders = [];
236 11
        $validator = function ($value, array $ranges) {
237 10
            return $this->validator->validate(
238
                $value,
239 10
                [new Ip(allowNegation: false, allowSubnet: false, ranges: $ranges)]
240
            );
241
        };
242
243 11
        foreach ($this->trustedHosts as $data) {
244
            // collect all trusted headers
245 10
            $trustedHeaders = array_merge($trustedHeaders, $data[self::DATA_KEY_TRUSTED_HEADERS]);
246
247 10
            if ($trustedHostData !== null) {
248
                // trusted hosts already found
249
                continue;
250
            }
251
252 10
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS], $validator)) {
253 8
                $trustedHostData = $data;
254
            }
255
        }
256
257
        /** @psalm-suppress PossiblyNullArgument, PossiblyNullArrayAccess */
258 11
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
259 11
        $request = $this->removeHeaders($request, $untrustedHeaders);
260
261 11
        if ($trustedHostData === null) {
262
            // No trusted host at all.
263 3
            return $this->handleNotTrusted($request, $handler);
264
        }
265
266 8
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
267 8
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
268
269 8
        if ($ipListType === null) {
270 2
            $hostList = $this->getFormattedIpList($hostList);
271 6
        } elseif ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
272 6
            $hostList = $this->getElementsByRfc7239($hostList);
273
        }
274
275 8
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
276 8
        $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
                if ($hostData === null) {
284
                    continue;
285
                }
286
287
                if (!isset($hostData['ip'])) {
288
                    break;
289
                }
290
            }
291
292 8
            $ip = $hostData['ip'];
293
294 8
            if (!$this->isValidHost($ip, ['any'], $validator)) {
295
                // invalid IP
296
                break;
297
            }
298
299 8
            $hostDataList[] = $hostData;
300
301 8
            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
        }
310
311 8
        $uri = $request->getUri();
312
        // find HTTP host
313 8
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
314 4
            if (!$request->hasHeader($hostHeader)) {
315
                continue;
316
            }
317
318
            if (
319 4
                $hostHeader === $ipHeader
320 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
321 4
                && isset($hostData['httpHost'])
322
            ) {
323 2
                $uri = $uri->withHost($hostData['httpHost']);
324 2
                break;
325
            }
326
327 2
            $host = $request->getHeaderLine($hostHeader);
328
329 2
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
330 2
                $uri = $uri->withHost($host);
331 2
                break;
332
            }
333
        }
334
335
        // find protocol
336 8
        foreach ($trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS] as $protocolHeader => $protocols) {
337 4
            if (!$request->hasHeader($protocolHeader)) {
338
                continue;
339
            }
340
341
            if (
342 4
                $protocolHeader === $ipHeader
343 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
344 4
                && isset($hostData['protocol'])
345
            ) {
346 4
                $uri = $uri->withScheme($hostData['protocol']);
347 4
                break;
348
            }
349
350 2
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
351
352 2
            foreach ($protocols as $protocol => $acceptedValues) {
353 2
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
354
                    $uri = $uri->withScheme($protocol);
355
                    break 2;
356
                }
357
            }
358
        }
359
360 8
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
361
362 8
        if ($urlParts !== null) {
363 3
            [$path, $query] = $urlParts;
364 3
            $uri = $uri->withPath($path);
365
366 3
            if ($query !== null) {
367 3
                $uri = $uri->withQuery($query);
368
            }
369
        }
370
371
        // find port
372 8
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
373 1
            if (!$request->hasHeader($portHeader)) {
374
                continue;
375
            }
376
377
            if (
378 1
                $portHeader === $ipHeader
379 1
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
380 1
                && isset($hostData['port'])
381 1
                && $this->checkPort((string) $hostData['port'])
382
            ) {
383 1
                $uri = $uri->withPort($hostData['port']);
384 1
                break;
385
            }
386
387
            $port = $request->getHeaderLine($portHeader);
388
389
            if ($this->checkPort($port)) {
390
                $uri = $uri->withPort((int) $port);
391
                break;
392
            }
393
        }
394
395 8
        return $handler->handle($request->withUri($uri)->withAttribute('requestClientIp', $hostData['ip'] ?? null));
396
    }
397
398
    /**
399
     * Validate host by range.
400
     *
401
     * This method can be extendable by overwriting e.g. with reverse DNS verification.
402
     */
403 10
    protected function isValidHost(string $host, array $ranges, Closure $validator): bool
404
    {
405 10
        return $validator($host,$ranges)->isValid();
406
    }
407
408
    /**
409
     * 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 4
    private function handleNotTrusted(
439
        ServerRequestInterface $request,
440
        RequestHandlerInterface $handler
441
    ): ResponseInterface {
442 4
        if ($this->attributeIps !== null) {
443 1
            $request = $request->withAttribute($this->attributeIps, null);
444
        }
445
446 4
        return $handler->handle($request->withAttribute('requestClientIp', null));
447
    }
448
449 19
    private function prepareProtocolHeaders(array $protocolHeaders): array
450
    {
451 19
        $output = [];
452
453 19
        foreach ($protocolHeaders as $header => $protocolAndAcceptedValues) {
454 8
            $header = strtolower($header);
455
456 8
            if (is_callable($protocolAndAcceptedValues)) {
457
                $output[$header] = $protocolAndAcceptedValues;
458
                continue;
459
            }
460
461 8
            if (!is_array($protocolAndAcceptedValues)) {
462 1
                throw new RuntimeException('Accepted values is not an array nor callable.');
463
            }
464
465 7
            if ($protocolAndAcceptedValues === []) {
466 1
                throw new RuntimeException('Accepted values cannot be an empty array.');
467
            }
468
469 6
            $output[$header] = [];
470
471 6
            foreach ($protocolAndAcceptedValues as $protocol => $acceptedValues) {
472 6
                if (!is_string($protocol)) {
473 1
                    throw new RuntimeException('The protocol must be a string.');
474
                }
475
476 5
                if ($protocol === '') {
477 1
                    throw new RuntimeException('The protocol cannot be empty.');
478
                }
479
480 4
                $output[$header][$protocol] = array_map('\strtolower', (array)$acceptedValues);
481
            }
482
        }
483
484 15
        return $output;
485
    }
486
487 11
    private function removeHeaders(ServerRequestInterface $request, array $headers): ServerRequestInterface
488
    {
489 11
        foreach ($headers as $header) {
490
            $request = $request->withoutAttribute($header);
491
        }
492
493 11
        return $request;
494
    }
495
496 8
    private function getIpList(RequestInterface $request, array $ipHeaders): array
497
    {
498 8
        foreach ($ipHeaders as $ipHeader) {
499 8
            $type = null;
500
501 8
            if (is_array($ipHeader)) {
502 6
                $type = array_shift($ipHeader);
503 6
                $ipHeader = array_shift($ipHeader);
504
            }
505
506 8
            if ($request->hasHeader($ipHeader)) {
507 8
                return [$type, $ipHeader, $request->getHeader($ipHeader)];
508
            }
509
        }
510
511
        return [null, null, []];
512
    }
513
514
    /**
515
     * @see getElementsByRfc7239
516
     */
517 2
    private function getFormattedIpList(array $forwards): array
518
    {
519 2
        $list = [];
520
521 2
        foreach ($forwards as $ip) {
522 2
            $list[] = ['ip' => $ip];
523
        }
524
525 2
        return $list;
526
    }
527
528
    /**
529
     * 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 6
    private function getElementsByRfc7239(array $forwards): array
546
    {
547 6
        $list = [];
548
549 6
        foreach ($forwards as $forward) {
550 6
            $data = HeaderValueHelper::getParameters($forward);
551
552 6
            if (!isset($data['for'])) {
553
                // Invalid item, the following items will be dropped
554
                break;
555
            }
556
557 6
            $pattern = '/^(?<host>' . IpHelper::IPV4_PATTERN . '|unknown|_[\w\.-]+|[[]'
558
                . IpHelper::IPV6_PATTERN . '[]])(?::(?<port>[\w\.-]+))?$/';
559
560 6
            if (preg_match($pattern, $data['for'], $matches) === 0) {
561
                // Invalid item, the following items will be dropped
562
                break;
563
            }
564
565 6
            $ipData = [];
566 6
            $host = $matches['host'];
567 6
            $obfuscatedHost = $host === 'unknown' || strpos($host, '_') === 0;
568
569 6
            if (!$obfuscatedHost) {
570
                // IPv4 & IPv6
571 6
                $ipData['ip'] = strpos($host, '[') === 0 ? trim($host /* IPv6 */, '[]') : $host;
572
            }
573
574 6
            $ipData['host'] = $host;
575
576 6
            if (isset($matches['port'])) {
577 1
                $port = $matches['port'];
578
579 1
                if (!$obfuscatedHost && !$this->checkPort($port)) {
580
                    // Invalid port, the following items will be dropped
581
                    break;
582
                }
583
584 1
                $ipData['port'] = $obfuscatedHost ? $port : (int)$port;
585
            }
586
587
            // copy other properties
588 6
            foreach (['proto' => 'protocol', 'host' => 'httpHost', 'by' => 'by'] as $source => $destination) {
589 6
                if (isset($data[$source])) {
590 4
                    $ipData[$destination] = $data[$source];
591
                }
592
            }
593
594 6
            if (isset($ipData['httpHost']) && filter_var($ipData['httpHost'], FILTER_VALIDATE_DOMAIN) === false) {
595
                // remove not valid HTTP host
596
                unset($ipData['httpHost']);
597
            }
598
599 6
            $list[] = $ipData;
600
        }
601
602 6
        return $list;
603
    }
604
605 8
    private function getUrl(RequestInterface $request, array $urlHeaders): ?array
606
    {
607 8
        foreach ($urlHeaders as $header) {
608 3
            if (!$request->hasHeader($header)) {
609
                continue;
610
            }
611
612 3
            $url = $request->getHeaderLine($header);
613
614 3
            if (strpos($url, '/') === 0) {
615 3
                return array_pad(explode('?', $url, 2), 2, null);
616
            }
617
        }
618
619 5
        return null;
620
    }
621
622 1
    private function checkPort(string $port): bool
623
    {
624 1
        return preg_match('/^\d{1,5}$/', $port) === 1 && (int)$port <= 65535;
625
    }
626
627 15
    private function checkTypeStringOrArray(array $array, string $field): void
628
    {
629 15
        foreach ($array as $item) {
630 15
            if (!is_string($item)) {
631 1
                throw new InvalidArgumentException("$field must be string type");
632
            }
633
634 14
            if (trim($item) === '') {
635 2
                throw new InvalidArgumentException("$field cannot be empty strings");
636
            }
637
        }
638
    }
639
}
640