Test Failed
Pull Request — master (#5)
by Dmitriy
02:20
created

TrustedHostsNetworkResolver::checkPort()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Middleware;
6
7
use InvalidArgumentException;
8
use PhpParser\Node\Expr\Closure;
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\Rule\Ip\Ip;
18
use Yiisoft\Validator\ValidatorInterface;
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 filter_var;
27
use function in_array;
28
use function is_array;
29
use function is_callable;
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
    private ?ValidatorInterface $validator = null;
92
93
    /**
94
     * Returns a new instance with the added trusted hosts and related headers.
95
     *
96
     * The header lists are evaluated in the order they were specified.
97
     * If you specify multiple headers by type (e.g. IP headers), you must ensure that the irrelevant header is removed
98
     * e.g. web server application, otherwise spoof clients can be use this vulnerability.
99
     *
100
     * @param string[] $hosts List of trusted hosts IP addresses. If {@see isValidHost()} method is extended,
101
     * then can use domain names with reverse DNS resolving e.g. yiiframework.com, * .yiiframework.com.
102
     * @param array $ipHeaders List of headers containing IP lists.
103
     * @param array $protocolHeaders List of headers containing protocol. e.g.
104
     * ['x-forwarded-for' => ['http' => 'http', 'https' => ['on', 'https']]].
105
     * @param string[] $hostHeaders List of headers containing HTTP host.
106
     * @param string[] $urlHeaders List of headers containing HTTP URL.
107
     * @param string[] $portHeaders List of headers containing port number.
108
     * @param string[]|null $trustedHeaders List of trusted headers. Removed from the request, if in checking process
109
     * are classified as untrusted by hosts.
110
     *
111
     * @return self
112 32
     */
113
    public function withAddedTrustedHosts(
114
        array $hosts,
115
        // Defining default headers is not secure!
116
        array $ipHeaders = [],
117
        array $protocolHeaders = [],
118
        array $hostHeaders = [],
119
        array $urlHeaders = [],
120
        array $portHeaders = [],
121
        ?array $trustedHeaders = null
122 32
    ): self {
123
        $new = clone $this;
124 32
125 16
        foreach ($ipHeaders as $ipHeader) {
126 5
            if (is_string($ipHeader)) {
127
                continue;
128
            }
129 11
130 1
            if (!is_array($ipHeader)) {
131
                throw new InvalidArgumentException('Type of IP header is not a string and not array.');
132
            }
133 10
134 1
            if (count($ipHeader) !== 2) {
135
                throw new InvalidArgumentException('The IP header array must have exactly 2 elements.');
136
            }
137 9
138
            [$type, $header] = $ipHeader;
139 9
140 1
            if (!is_string($type)) {
141
                throw new InvalidArgumentException('The IP header type is not a string.');
142
            }
143 8
144 1
            if (!is_string($header)) {
145
                throw new InvalidArgumentException('The IP header value is not a string.');
146
            }
147 7
148 6
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
149
                continue;
150
            }
151 1
152
            throw new InvalidArgumentException("Not supported IP header type: $type.");
153
        }
154 27
155 8
        if ($hosts === []) {
156
            throw new InvalidArgumentException('Empty hosts not allowed.');
157
        }
158 19
159 19
        $trustedHeaders = $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS;
160
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
161 15
162 12
        $this->checkTypeStringOrArray($hosts, 'hosts');
163 12
        $this->checkTypeStringOrArray($trustedHeaders, 'trustedHeaders');
164 12
        $this->checkTypeStringOrArray($hostHeaders, 'hostHeaders');
165 12
        $this->checkTypeStringOrArray($urlHeaders, 'urlHeaders');
166
        $this->checkTypeStringOrArray($portHeaders, 'portHeaders');
167 12
168 12
        foreach ($hosts as $host) {
169
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
170 12
171 1
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
172
                throw new InvalidArgumentException("\"$host\" host is not a domain and not an IP address.");
173
            }
174
        }
175 11
176 11
        $new->trustedHosts[] = [
177 11
            self::DATA_KEY_HOSTS => $hosts,
178 11
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
179 11
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
180 11
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
181 11
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
182 11
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
183
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
184
        ];
185 11
186
        return $new;
187
    }
188
189
    /**
190
     * Returns a new instance without the trusted hosts and related headers.
191
     *
192
     * @return self
193 1
     */
194
    public function withoutTrustedHosts(): self
195 1
    {
196 1
        $new = clone $this;
197 1
        $new->trustedHosts = [];
198
        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
     * @return self
209 3
     */
210
    public function withAttributeIps(?string $attribute): self
211 3
    {
212 1
        if ($attribute === '') {
213
            throw new RuntimeException('Attribute should not be empty string.');
214
        }
215 2
216 2
        $new = clone $this;
217 2
        $new->attributeIps = $attribute;
218
        return $new;
219
    }
220
221
    /**
222
     * Returns a new instance with the specified client IP validator.
223
     *
224
     * @param Ip $validator Client IP validator.
225
     *
226
     * @return self
227 1
     */
228
    public function withValidator(ValidatorInterface $validator): self
229 1
    {
230 1
        $new = clone $this;
231 1
        $new->validator = $validator;
232
        return $new;
233
    }
234 12
235
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
236 12
    {
237
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
238 12
239
        if ($actualHost === null) {
240 1
            // Validation is not possible.
241
            return $this->handleNotTrusted($request, $handler);
242
        }
243 11
244 11
        $trustedHostData = null;
245 11
        $trustedHeaders = [];
246
        $validator = function ($value, array $ranges) {
247 11
            return $this->validator->validate(
0 ignored issues
show
Bug introduced by
The method validate() does not exist on null. ( Ignorable by Annotation )

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

247
            return $this->validator->/** @scrutinizer ignore-call */ validate(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
248
                $value,
249 10
                [new Ip(allowNegation: false, allowSubnet: false, ranges: $ranges)]
250
            );
251 10
        };
252
253
        foreach ($this->trustedHosts as $data) {
254
            // collect all trusted headers
255
            $trustedHeaders = array_merge($trustedHeaders, $data[self::DATA_KEY_TRUSTED_HEADERS]);
256 10
257 8
            if ($trustedHostData !== null) {
258
                // trusted hosts already found
259
                continue;
260
            }
261
262 11
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS], $validator)) {
263 11
                $trustedHostData = $data;
264
            }
265 11
        }
266
267 3
        /** @psalm-suppress PossiblyNullArgument, PossiblyNullArrayAccess */
268
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
269
        $request = $this->removeHeaders($request, $untrustedHeaders);
270 8
271 8
        if ($trustedHostData === null) {
272
            // No trusted host at all.
273 8
            return $this->handleNotTrusted($request, $handler);
274 2
        }
275 6
276 6
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
277
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
278
279 8
        if ($ipListType === null) {
280 8
            $hostList = $this->getFormattedIpList($hostList);
281
        } elseif ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
282
            $hostList = $this->getElementsByRfc7239($hostList);
283 8
        }
284 8
285
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
286
        $hostDataList = [];
287
288
        do {
289
            $hostData = array_shift($hostList);
290
            if (!isset($hostData['ip'])) {
291
                $hostData = $this->reverseObfuscate($hostData, $hostDataList, $hostList, $request);
292
293
                if ($hostData === null) {
294
                    continue;
295
                }
296 8
297
                if (!isset($hostData['ip'])) {
298 8
                    break;
299
                }
300
            }
301
302
            $ip = $hostData['ip'];
303 8
304
            if (!$this->isValidHost($ip, ['any'], $validator)) {
305 8
                // invalid IP
306
                break;
307 8
            }
308
309 8
            $hostDataList[] = $hostData;
310
311 8
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS], $validator)) {
312
                // not trusted host
313
                break;
314
            }
315 8
        } while (count($hostList) > 0);
316
317 8
        if ($this->attributeIps !== null) {
318 4
            $request = $request->withAttribute($this->attributeIps, $hostDataList);
319
        }
320
321
        $uri = $request->getUri();
322
        // find HTTP host
323 4
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
324 4
            if (!$request->hasHeader($hostHeader)) {
325 4
                continue;
326
            }
327 2
328 2
            if (
329
                $hostHeader === $ipHeader
330
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
331 2
                && isset($hostData['httpHost'])
332
            ) {
333 2
                $uri = $uri->withHost($hostData['httpHost']);
334 2
                break;
335 2
            }
336
337
            $host = $request->getHeaderLine($hostHeader);
338
339
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
340 8
                $uri = $uri->withHost($host);
341 4
                break;
342
            }
343
        }
344
345
        // find protocol
346 4
        foreach ($trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS] as $protocolHeader => $protocols) {
347 4
            if (!$request->hasHeader($protocolHeader)) {
348 4
                continue;
349
            }
350 4
351 4
            if (
352
                $protocolHeader === $ipHeader
353
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
354 2
                && isset($hostData['protocol'])
355
            ) {
356 2
                $uri = $uri->withScheme($hostData['protocol']);
357 2
                break;
358
            }
359
360
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
361
362
            foreach ($protocols as $protocol => $acceptedValues) {
363
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
364 8
                    $uri = $uri->withScheme($protocol);
365
                    break 2;
366 8
                }
367 3
            }
368 3
        }
369
370 3
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
371 3
372
        if ($urlParts !== null) {
373
            [$path, $query] = $urlParts;
374
            $uri = $uri->withPath($path);
375
376 8
            if ($query !== null) {
377 1
                $uri = $uri->withQuery($query);
378
            }
379
        }
380
381
        // find port
382 1
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
383 1
            if (!$request->hasHeader($portHeader)) {
384 1
                continue;
385 1
            }
386
387 1
            if (
388 1
                $portHeader === $ipHeader
389
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
390
                && isset($hostData['port'])
391
                && $this->checkPort((string) $hostData['port'])
392
            ) {
393
                $uri = $uri->withPort($hostData['port']);
394
                break;
395
            }
396
397
            $port = $request->getHeaderLine($portHeader);
398
399 8
            if ($this->checkPort($port)) {
400
                $uri = $uri->withPort((int) $port);
401
                break;
402
            }
403
        }
404
405
        return $handler->handle($request->withUri($uri)->withAttribute('requestClientIp', $hostData['ip'] ?? null));
406
    }
407 10
408
    /**
409 10
     * Validate host by range.
410
     *
411
     * This method can be extendable by overwriting e.g. with reverse DNS verification.
412
     */
413
    protected function isValidHost(string $host, array $ranges, \Closure $validator): bool
414
    {
415
        return $validator($host,$ranges)->isValid();
416
    }
417
418
    /**
419
     * Reverse obfuscating host data
420
     *
421
     * RFC 7239 allows to use obfuscated host data. In this case, either specifying the
422
     * IP address or dropping the proxy endpoint is required to determine validated route.
423
     *
424
     * By default it does not perform any transformation on the data. You can override this method.
425
     *
426
     * @param array $hostData
427
     * @param array $hostDataListValidated
428
     * @param array $hostDataListRemaining
429
     * @param RequestInterface $request
430
     *
431
     * @return array|null reverse obfuscated host data or null.
432
     * In case of null data is discarded and the process continues with the next portion of host data.
433
     * If the return value is an array, it must contain at least the `ip` key.
434
     *
435
     * @see getElementsByRfc7239()
436
     * @link https://tools.ietf.org/html/rfc7239#section-6.2
437
     * @link https://tools.ietf.org/html/rfc7239#section-6.3
438
     */
439
    protected function reverseObfuscate(
440
        array $hostData,
441
        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

441
        /** @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...
442 4
        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

442
        /** @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...
443
        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

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