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

prepareProtocolHeaders()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 36
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8.0747

Importance

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

437
        /** @scrutinizer ignore-unused */ array $hostDataListValidated,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
438
        array $hostDataListRemaining,
0 ignored issues
show
Unused Code introduced by
The parameter $hostDataListRemaining is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

438
        /** @scrutinizer ignore-unused */ array $hostDataListRemaining,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
439
        RequestInterface $request
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

439
        /** @scrutinizer ignore-unused */ RequestInterface $request

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

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