Test Failed
Pull Request — master (#6)
by Rustam
12:16
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 58
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 15.3888

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