Passed
Push — master ( fdd91d...4a80ba )
by Rustam
02:35
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 58
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 15.7308

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 27
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 58
ccs 23
cts 27
cp 0.8519
crap 15.7308
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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