Test Failed
Pull Request — master (#5)
by Dmitriy
02:20
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 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