Passed
Pull Request — master (#42)
by Rustam
03:31 queued 32s
created

getElementsByRfc7239()   C

Complexity

Conditions 15
Paths 9

Size

Total Lines 59
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 15.0818

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 15
eloc 27
c 2
b 0
f 0
nc 9
nop 1
dl 0
loc 59
ccs 26
cts 28
cp 0.9286
crap 15.0818
rs 5.9166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Middleware;
6
7
use Closure;
8
use InvalidArgumentException;
9
use Psr\Http\Message\RequestInterface;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\ServerRequestInterface;
12
use Psr\Http\Server\MiddlewareInterface;
13
use Psr\Http\Server\RequestHandlerInterface;
14
use RuntimeException;
15
use Yiisoft\Http\HeaderValueHelper;
16
use Yiisoft\NetworkUtilities\IpHelper;
17
use Yiisoft\Validator\Result;
18
use Yiisoft\Validator\Rule\Ip;
19
use Yiisoft\Validator\ValidatorInterface;
20
21
use function array_diff;
22
use function array_pad;
23
use function array_reverse;
24
use function array_shift;
25
use function array_unshift;
26
use function count;
27
use function explode;
28
use function filter_var;
29
use function in_array;
30
use function is_array;
31
use function is_callable;
32
use function is_string;
33
use function preg_match;
34
use function str_replace;
35
use function str_starts_with;
36
use function strtolower;
37
use function trim;
38
39
/**
40
 * Trusted hosts network resolver.
41
 *
42
 * ```php
43
 * $trustedHostsNetworkResolver->withAddedTrustedHosts(
44
 *   // List of secure hosts including $_SERVER['REMOTE_ADDR'], can specify IPv4, IPv6, domains and aliases {@see Ip}.
45
 *   ['1.1.1.1', '2.2.2.1/3', '2001::/32', 'localhost'].
46
 *   // IP list headers. For advanced handling headers {@see TrustedHostsNetworkResolver::IP_HEADER_TYPE_RFC7239}.
47
 *   // Headers containing multiple sub-elements (e.g. RFC 7239) must also be listed for other relevant types
48
 *   // (e.g. host headers), otherwise they will only be used as an IP list.
49
 *   ['x-forwarded-for', [TrustedHostsNetworkResolver::IP_HEADER_TYPE_RFC7239, 'forwarded']]
50
 *   // Protocol headers with accepted protocols and values. Matching of values is case-insensitive.
51
 *   ['front-end-https' => ['https' => 'on']],
52
 *   // Host headers
53
 *   ['forwarded', 'x-forwarded-for']
54
 *   // URL headers
55
 *   ['x-rewrite-url'],
56
 *   // Port headers
57
 *   ['x-rewrite-port'],
58
 *   // Trusted headers. It is a good idea to list all relevant headers.
59
 *   ['x-forwarded-for', 'forwarded', ...],
60
 * );
61
 * ```
62
 *
63
 * @psalm-type HostData = array{ip?:string, host?: string, by?: string, port?: string|int, protocol?: string, httpHost?: string}
64
 * @psalm-type ProtocolHeadersData = array<string, array<non-empty-string, array<array-key, string>>|callable>
65
 * @psalm-type TrustedHostData = non-empty-array<self::DATA_KEY_*, array<string>>
66
 */
67
class TrustedHostsNetworkResolver implements MiddlewareInterface
68
{
69
    public const REQUEST_CLIENT_IP = 'requestClientIp';
70
    public const IP_HEADER_TYPE_RFC7239 = 'rfc7239';
71
72
    public const DEFAULT_TRUSTED_HEADERS = [
73
        // common:
74
        'x-forwarded-for',
75
        'x-forwarded-host',
76
        'x-forwarded-proto',
77
        'x-forwarded-port',
78
79
        // RFC:
80
        'forward',
81
82
        // Microsoft:
83
        'front-end-https',
84
        'x-rewrite-url',
85
    ];
86
87
    private const DATA_KEY_HOSTS = 'hosts';
88
    private const DATA_KEY_IP_HEADERS = 'ipHeaders';
89
    private const DATA_KEY_HOST_HEADERS = 'hostHeaders';
90
    private const DATA_KEY_URL_HEADERS = 'urlHeaders';
91
    private const DATA_KEY_PROTOCOL_HEADERS = 'protocolHeaders';
92
    private const DATA_KEY_TRUSTED_HEADERS = 'trustedHeaders';
93
    private const DATA_KEY_PORT_HEADERS = 'portHeaders';
94
95
    /**
96
     * @var array<TrustedHostData>
97
     */
98
    private array $trustedHosts = [];
99
    private ?string $attributeIps = null;
100
101 41
    public function __construct(private ValidatorInterface $validator)
102
    {
103 41
    }
104
105
    /**
106
     * Returns a new instance with the added trusted hosts and related headers.
107
     *
108
     * The header lists are evaluated in the order they were specified.
109
     * If you specify multiple headers by type (e.g. IP headers), you must ensure that the irrelevant header is removed
110
     * e.g. web server application, otherwise spoof clients can be use this vulnerability.
111
     *
112
     * @param string[] $hosts List of trusted hosts IP addresses. If {@see isValidHost()} method is extended,
113
     * then can use domain names with reverse DNS resolving e.g. yiiframework.com, * .yiiframework.com.
114
     * @param array $ipHeaders List of headers containing IP lists.
115
     * @param array $protocolHeaders List of headers containing protocol. e.g.
116
     * ['x-forwarded-for' => ['http' => 'http', 'https' => ['on', 'https']]].
117
     * @param string[] $hostHeaders List of headers containing HTTP host.
118
     * @param string[] $urlHeaders List of headers containing HTTP URL.
119
     * @param string[] $portHeaders List of headers containing port number.
120
     * @param string[]|null $trustedHeaders List of trusted headers. Removed from the request, if in checking process
121
     * are classified as untrusted by hosts.
122
     */
123 38
    public function withAddedTrustedHosts(
124
        array $hosts,
125
        // Defining default headers is not secure!
126
        array $ipHeaders = [],
127
        array $protocolHeaders = [],
128
        array $hostHeaders = [],
129
        array $urlHeaders = [],
130
        array $portHeaders = [],
131
        ?array $trustedHeaders = null,
132
    ): self {
133 38
        $new = clone $this;
134
135 38
        foreach ($ipHeaders as $ipHeader) {
136 20
            if (is_string($ipHeader)) {
137 6
                continue;
138
            }
139
140 14
            if (!is_array($ipHeader)) {
141 1
                throw new InvalidArgumentException('Type of IP header is not a string and not array.');
142
            }
143
144 13
            if (count($ipHeader) !== 2) {
145 1
                throw new InvalidArgumentException('The IP header array must have exactly 2 elements.');
146
            }
147
148 12
            [$type, $header] = $ipHeader;
149
150 12
            if (!is_string($type)) {
151 1
                throw new InvalidArgumentException('The IP header type is not a string.');
152
            }
153
154 11
            if (!is_string($header)) {
155 1
                throw new InvalidArgumentException('The IP header value is not a string.');
156
            }
157
158 10
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
159 9
                continue;
160
            }
161
162 1
            throw new InvalidArgumentException("Not supported IP header type: $type.");
163
        }
164
165 33
        if ($hosts === []) {
166 8
            throw new InvalidArgumentException('Empty hosts not allowed.');
167
        }
168
169 25
        $trustedHeaders ??= self::DEFAULT_TRUSTED_HEADERS;
170
        /** @var array|ProtocolHeadersData $protocolHeaders */
171 25
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
0 ignored issues
show
Bug introduced by
It seems like $protocolHeaders can also be of type Yiisoft\Yii\Middleware\ProtocolHeadersData; however, parameter $protocolHeaders of Yiisoft\Yii\Middleware\T...repareProtocolHeaders() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

171
        $protocolHeaders = $this->prepareProtocolHeaders(/** @scrutinizer ignore-type */ $protocolHeaders);
Loading history...
172
173 21
        $this->checkTypeStringOrArray($hosts, self::DATA_KEY_HOSTS);
174 18
        $this->checkTypeStringOrArray($trustedHeaders, self::DATA_KEY_TRUSTED_HEADERS);
175 18
        $this->checkTypeStringOrArray($hostHeaders, self::DATA_KEY_HOST_HEADERS);
176 18
        $this->checkTypeStringOrArray($urlHeaders, self::DATA_KEY_URL_HEADERS);
177 18
        $this->checkTypeStringOrArray($portHeaders, self::DATA_KEY_PORT_HEADERS);
178
179 18
        foreach ($hosts as $host) {
180 18
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
181
182 18
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
183 1
                throw new InvalidArgumentException("\"$host\" host is not a domain and not an IP address.");
184
            }
185
        }
186
187
        /** @var array<array-key, string> $ipHeaders */
188 17
        $new->trustedHosts[] = [
189 17
            self::DATA_KEY_HOSTS => $hosts,
190 17
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
191 17
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
192 17
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
193 17
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
194 17
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
195 17
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
196 17
        ];
197
198 17
        return $new;
199
    }
200
201
    /**
202
     * Returns a new instance without the trusted hosts and related headers.
203
     */
204 1
    public function withoutTrustedHosts(): self
205
    {
206 1
        $new = clone $this;
207 1
        $new->trustedHosts = [];
208 1
        return $new;
209
    }
210
211
    /**
212
     * Returns a new instance with the specified request's attribute name to which trusted path data is added.
213
     *
214
     * @param string|null $attribute The request attribute name.
215
     *
216
     * @see getElementsByRfc7239()
217
     */
218 3
    public function withAttributeIps(?string $attribute): self
219
    {
220 3
        if ($attribute === '') {
221 1
            throw new RuntimeException('Attribute should not be empty string.');
222
        }
223
224 2
        $new = clone $this;
225 2
        $new->attributeIps = $attribute;
226 2
        return $new;
227
    }
228
229 18
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
230
    {
231
        /** @var string|null $actualHost */
232 18
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
233
234 18
        if ($actualHost === null) {
235
            // Validation is not possible.
236 1
            return $this->handleNotTrusted($request, $handler);
237
        }
238
239 17
        $trustedHostData = null;
240 17
        $trustedHeaders = [];
241
242 17
        foreach ($this->trustedHosts as $data) {
243
            // collect all trusted headers
244 16
            $trustedHeaders[] = $data[self::DATA_KEY_TRUSTED_HEADERS];
245
246 16
            if ($trustedHostData !== null) {
247
                // trusted hosts already found
248 1
                continue;
249
            }
250
251 16
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS])) {
252 14
                $trustedHostData = $data;
253
            }
254
        }
255
256 17
        if ($trustedHostData === null) {
257
            // No trusted host at all.
258 3
            return $this->handleNotTrusted($request, $handler);
259
        }
260
261 14
        $trustedHeaders = array_merge(...$trustedHeaders);
262 14
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
263 14
        $request = $this->removeHeaders($request, $untrustedHeaders);
264
265 14
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
266 14
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
267
268 14
        if ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
269 9
            $hostList = $this->getElementsByRfc7239($hostList);
270
        } else {
271 5
            $hostList = $this->getFormattedIpList($hostList);
272
        }
273
274 14
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
275 14
        $hostDataList = [];
276
277
        do {
278 14
            $hostData = array_shift($hostList);
279 14
            if (!isset($hostData['ip'])) {
280
                $hostData = $this->reverseObfuscate($hostData, $hostDataList, $hostList, $request);
281
282
                if ($hostData === null) {
283
                    continue;
284
                }
285
286
                if (!isset($hostData['ip'])) {
287
                    break;
288
                }
289
            }
290
291 14
            $ip = $hostData['ip'];
292
293 14
            if (!$this->isValidHost($ip, ['any'])) {
294
                // invalid IP
295
                break;
296
            }
297
298 14
            $hostDataList[] = $hostData;
299
300 14
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS])) {
301
                // not trusted host
302 11
                break;
303
            }
304 14
        } while (count($hostList) > 0);
305
306 14
        if ($this->attributeIps !== null) {
307
            $request = $request->withAttribute($this->attributeIps, $hostDataList);
308
        }
309
310 14
        $uri = $request->getUri();
311
        // find HTTP host
312 14
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
313 4
            if (!$request->hasHeader($hostHeader)) {
314
                continue;
315
            }
316
317
            if (
318 4
                $hostHeader === $ipHeader
319 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
320 4
                && isset($hostData['httpHost'])
321
            ) {
322 2
                $uri = $uri->withHost($hostData['httpHost']);
323 2
                break;
324
            }
325
326 2
            $host = $request->getHeaderLine($hostHeader);
327
328 2
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
329 2
                $uri = $uri->withHost($host);
330 2
                break;
331
            }
332
        }
333
334
        // find protocol
335
        /** @psalm-var ProtocolHeadersData $protocolHeadersData */
336 14
        $protocolHeadersData = $trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS];
337 14
        foreach ($protocolHeadersData as $protocolHeader => $protocols) {
338 4
            if (!$request->hasHeader($protocolHeader)) {
339
                continue;
340
            }
341
342
            if (
343 4
                $protocolHeader === $ipHeader
344 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
345 4
                && isset($hostData['protocol'])
346
            ) {
347 4
                $uri = $uri->withScheme($hostData['protocol']);
348 4
                break;
349
            }
350
351 2
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
352
353 2
            foreach ($protocols as $protocol => $acceptedValues) {
354 2
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
355
                    $uri = $uri->withScheme($protocol);
356
                    break 2;
357
                }
358
            }
359
        }
360
361 14
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
362
363 14
        if ($urlParts !== null) {
364 3
            [$path, $query] = $urlParts;
365 3
            if ($path !== null) {
366 3
                $uri = $uri->withPath($path);
367
            }
368
369 3
            if ($query !== null) {
370 3
                $uri = $uri->withQuery($query);
371
            }
372
        }
373
374
        // find port
375 14
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
376 2
            if (!$request->hasHeader($portHeader)) {
377
                continue;
378
            }
379
380
            if (
381 2
                $portHeader === $ipHeader
382 2
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
383 2
                && isset($hostData['port'])
384 2
                && $this->checkPort((string) $hostData['port'])
385
            ) {
386 1
                $uri = $uri->withPort((int) $hostData['port']);
387 1
                break;
388
            }
389
390 1
            $port = $request->getHeaderLine($portHeader);
391
392 1
            if ($this->checkPort($port)) {
393 1
                $uri = $uri->withPort((int) $port);
394 1
                break;
395
            }
396
        }
397
398 14
        return $handler->handle(
399 14
            $request->withUri($uri)->withAttribute(self::REQUEST_CLIENT_IP, $hostData['ip'] ?? null)
400 14
        );
401
    }
402
403
    /**
404
     * Validate host by range.
405
     *
406
     * This method can be extendable by overwriting e.g. with reverse DNS verification.
407
     *
408
     * @param string[] $ranges
409
     * @param Closure(string, string[]): Result $validator
410
     */
411 16
    protected function isValidHost(string $host, array $ranges): bool
412
    {
413 16
        $result = $this->validator->validate(
414 16
            $host,
415 16
            [new Ip(allowSubnet: false, allowNegation: false, ranges: $ranges)]
416 16
        );
417 16
        return $result->isValid();
418
    }
419
420
    /**
421
     * Reverse obfuscating host data
422
     *
423
     * RFC 7239 allows to use obfuscated host data. In this case, either specifying the
424
     * IP address or dropping the proxy endpoint is required to determine validated route.
425
     *
426
     * By default, it does not perform any transformation on the data. You can override this method.
427
     *
428
     * @param HostData|null $hostData
0 ignored issues
show
Bug introduced by
The type Yiisoft\Yii\Middleware\HostData was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
429
     * @param array $hostDataListValidated
430
     * @param array $hostDataListRemaining
431
     * @param RequestInterface $request
432
     *
433
     * @return HostData|null reverse obfuscated host data or null.
434
     * In case of null data is discarded and the process continues with the next portion of host data.
435
     * If the return value is an array, it must contain at least the `ip` key.
436
     *
437
     * @see getElementsByRfc7239()
438
     * @link https://tools.ietf.org/html/rfc7239#section-6.2
439
     * @link https://tools.ietf.org/html/rfc7239#section-6.3
440
     */
441
    protected function reverseObfuscate(
442
        ?array $hostData,
443
        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

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

444
        /** @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...
445
        RequestInterface $request
446
    ): ?array {
447
        return $hostData;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $hostData returns the type array which is incompatible with the documented return type Yiisoft\Yii\Middleware\HostData|null.
Loading history...
448
    }
449
450 4
    private function handleNotTrusted(
451
        ServerRequestInterface $request,
452
        RequestHandlerInterface $handler
453
    ): ResponseInterface {
454 4
        if ($this->attributeIps !== null) {
455 1
            $request = $request->withAttribute($this->attributeIps, null);
456
        }
457
458 4
        return $handler->handle($request->withAttribute(self::REQUEST_CLIENT_IP, null));
459
    }
460
461
    /**
462
     * @return ProtocolHeadersData
0 ignored issues
show
Bug introduced by
The type Yiisoft\Yii\Middleware\ProtocolHeadersData was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
463
     */
464 25
    private function prepareProtocolHeaders(array $protocolHeaders): array
465
    {
466 25
        $output = [];
467
468 25
        foreach ($protocolHeaders as $header => $protocolAndAcceptedValues) {
469 8
            if (!is_string($header)) {
470
                throw new RuntimeException('The protocol header must be a string.');
471
            }
472 8
            $header = strtolower($header);
473
474 8
            if (is_callable($protocolAndAcceptedValues)) {
475 1
                $output[$header] = $protocolAndAcceptedValues;
476 1
                continue;
477
            }
478
479 7
            if (!is_array($protocolAndAcceptedValues)) {
480 1
                throw new RuntimeException('Accepted values is not an array nor callable.');
481
            }
482
483 6
            if ($protocolAndAcceptedValues === []) {
484 1
                throw new RuntimeException('Accepted values cannot be an empty array.');
485
            }
486
487 5
            $output[$header] = [];
488
489
            /**
490
             * @var array<string|string[]> $protocolAndAcceptedValues
491
             */
492 5
            foreach ($protocolAndAcceptedValues as $protocol => $acceptedValues) {
493 5
                if (!is_string($protocol)) {
494 1
                    throw new RuntimeException('The protocol must be a string.');
495
                }
496
497 4
                if ($protocol === '') {
498 1
                    throw new RuntimeException('The protocol cannot be empty.');
499
                }
500
501 3
                $output[$header][$protocol] = array_map('\strtolower', (array) $acceptedValues);
502
            }
503
        }
504
505 21
        return $output;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $output returns the type array which is incompatible with the documented return type Yiisoft\Yii\Middleware\ProtocolHeadersData.
Loading history...
506
    }
507
508
    /**
509
     * @param string[] $headers
510
     */
511 14
    private function removeHeaders(ServerRequestInterface $request, array $headers): ServerRequestInterface
512
    {
513 14
        foreach ($headers as $header) {
514
            $request = $request->withoutAttribute($header);
515
        }
516
517 14
        return $request;
518
    }
519
520
    /**
521
     * @param array<string|string[]> $ipHeaders
522
     *
523
     * @return array{0: string|null, 1: string|null, 2: string[]}
524
     */
525 14
    private function getIpList(ServerRequestInterface $request, array $ipHeaders): array
526
    {
527 14
        foreach ($ipHeaders as $ipHeader) {
528 12
            $type = null;
529
530 12
            if (is_array($ipHeader)) {
531 9
                $type = array_shift($ipHeader);
532 9
                $ipHeader = array_shift($ipHeader);
533
            }
534
535 12
            if ($request->hasHeader($ipHeader)) {
536 12
                return [$type, $ipHeader, $request->getHeader($ipHeader)];
537
            }
538
        }
539
540 2
        return [null, null, []];
541
    }
542
543
    /**
544
     * @param string[] $forwards
545
     *
546
     * @return list<HostData>
0 ignored issues
show
Bug introduced by
The type Yiisoft\Yii\Middleware\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
547
     *
548
     * @see getElementsByRfc7239()
549
     */
550 5
    private function getFormattedIpList(array $forwards): array
551
    {
552 5
        $list = [];
553
554 5
        foreach ($forwards as $ip) {
555 3
            $list[] = ['ip' => $ip];
556
        }
557
558 5
        return $list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $list returns the type array|array<mixed,array<string,string>> which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
559
    }
560
561
    /**
562
     * Forwarded elements by RFC7239.
563
     *
564
     * The structure of the elements:
565
     * - `host`: IP or obfuscated hostname or "unknown"
566
     * - `ip`: IP address (only if presented)
567
     * - `by`: used user-agent by proxy (only if presented)
568
     * - `port`: port number received by proxy (only if presented)
569
     * - `protocol`: protocol received by proxy (only if presented)
570
     * - `httpHost`: HTTP host received by proxy (only if presented)
571
     *
572
     * The list starts with the server and the last item is the client itself.
573
     *
574
     * @link https://tools.ietf.org/html/rfc7239
575
     *
576
     * @param string[] $forwards
577
     *
578
     * @return list<HostData> Proxy data elements.
579
     */
580 9
    private function getElementsByRfc7239(array $forwards): array
581
    {
582 9
        $list = [];
583
584 9
        foreach ($forwards as $forward) {
585
            /** @var array<string, string> $data */
586 9
            $data = HeaderValueHelper::getParameters($forward);
587
588 9
            if (!isset($data['for'])) {
589
                // Invalid item, the following items will be dropped
590 2
                break;
591
            }
592
593 8
            $pattern = '/^(?<host>' . IpHelper::IPV4_PATTERN . '|unknown|_[\w\.-]+|[[]'
594 8
                . IpHelper::IPV6_PATTERN . '[]])(?::(?<port>[\w\.-]+))?$/';
595
596 8
            if (preg_match($pattern, $data['for'], $matches) === 0) {
597
                // Invalid item, the following items will be dropped
598 1
                break;
599
            }
600
601 8
            $ipData = [];
602 8
            $host = $matches['host'];
603 8
            $obfuscatedHost = $host === 'unknown' || str_starts_with($host, '_');
604
605 8
            if (!$obfuscatedHost) {
606
                // IPv4 & IPv6
607 8
                $ipData['ip'] = str_starts_with($host, '[') ? trim($host /* IPv6 */, '[]') : $host;
608
            }
609
610 8
            $ipData['host'] = $host;
611
612 8
            if (isset($matches['port'])) {
613 1
                $port = $matches['port'];
614
615 1
                if (!$obfuscatedHost && !$this->checkPort($port)) {
616
                    // Invalid port, the following items will be dropped
617
                    break;
618
                }
619
620 1
                $ipData['port'] = $obfuscatedHost ? $port : (int) $port;
621
            }
622
623
            // copy other properties
624 8
            foreach (['proto' => 'protocol', 'host' => 'httpHost', 'by' => 'by'] as $source => $destination) {
625 8
                if (isset($data[$source])) {
626 4
                    $ipData[$destination] = $data[$source];
627
                }
628
            }
629
630 8
            if (isset($ipData['httpHost']) && filter_var($ipData['httpHost'], FILTER_VALIDATE_DOMAIN) === false) {
631
                // remove not valid HTTP host
632
                unset($ipData['httpHost']);
633
            }
634
635 8
            $list[] = $ipData;
636
        }
637
638 9
        return $list;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $list returns the type array|array<mixed,array> which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
639
    }
640
641
    /**
642
     * @param string[] $urlHeaders
643
     *
644
     * @psalm-return non-empty-list<null|string>|null
645
     */
646 14
    private function getUrl(RequestInterface $request, array $urlHeaders): ?array
647
    {
648 14
        foreach ($urlHeaders as $header) {
649 3
            if (!$request->hasHeader($header)) {
650 1
                continue;
651
            }
652
653 3
            $url = $request->getHeaderLine($header);
654
655 3
            if (str_starts_with($url, '/')) {
656 3
                return array_pad(explode('?', $url, 2), 2, null);
657
            }
658
        }
659
660 11
        return null;
661
    }
662
663 2
    private function checkPort(string $port): bool
664
    {
665 2
        return preg_match('/^\d{1,5}$/', $port) === 1 && (int) $port <= 65535;
666
    }
667
668
    /**
669
     * @psalm-assert array<non-empty-string> $array
670
     */
671 21
    private function checkTypeStringOrArray(array $array, string $field): void
672
    {
673 21
        foreach ($array as $item) {
674 21
            if (!is_string($item)) {
675 1
                throw new InvalidArgumentException("$field must be string type");
676
            }
677
678 20
            if (trim($item) === '') {
679 2
                throw new InvalidArgumentException("$field cannot be empty strings");
680
            }
681
        }
682
    }
683
}
684