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

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

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

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