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

451
        /** @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...
452
        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

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