Passed
Pull Request — master (#42)
by Rustam
11:52
created

TrustedHostsNetworkResolver::getFormattedIpList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

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

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

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