Passed
Pull Request — master (#42)
by Rustam
03:25
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_*, non-empty-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 25
        $protocolHeaders = $this->prepareProtocolHeaders($protocolHeaders);
171
172 21
        $this->checkTypeStringOrArray($hosts, self::DATA_KEY_HOSTS);
173 18
        $this->checkTypeStringOrArray($trustedHeaders, self::DATA_KEY_TRUSTED_HEADERS);
174 18
        $this->checkTypeStringOrArray($hostHeaders, self::DATA_KEY_HOST_HEADERS);
175 18
        $this->checkTypeStringOrArray($urlHeaders, self::DATA_KEY_URL_HEADERS);
176 18
        $this->checkTypeStringOrArray($portHeaders, self::DATA_KEY_PORT_HEADERS);
177
178 18
        foreach ($hosts as $host) {
179 18
            $host = str_replace('*', 'wildcard', $host); // wildcard is allowed in host
180
181 18
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
182 1
                throw new InvalidArgumentException("\"$host\" host is not a domain and not an IP address.");
183
            }
184
        }
185
186 17
        $new->trustedHosts[] = [
187 17
            self::DATA_KEY_HOSTS => $hosts,
188 17
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
189 17
            self::DATA_KEY_PROTOCOL_HEADERS => $protocolHeaders,
190 17
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders,
191 17
            self::DATA_KEY_HOST_HEADERS => $hostHeaders,
192 17
            self::DATA_KEY_URL_HEADERS => $urlHeaders,
193 17
            self::DATA_KEY_PORT_HEADERS => $portHeaders,
194 17
        ];
195
196 17
        return $new;
197
    }
198
199
    /**
200
     * Returns a new instance without the trusted hosts and related headers.
201
     */
202 1
    public function withoutTrustedHosts(): self
203
    {
204 1
        $new = clone $this;
205 1
        $new->trustedHosts = [];
206 1
        return $new;
207
    }
208
209
    /**
210
     * Returns a new instance with the specified request's attribute name to which trusted path data is added.
211
     *
212
     * @param string|null $attribute The request attribute name.
213
     *
214
     * @see getElementsByRfc7239()
215
     */
216 3
    public function withAttributeIps(?string $attribute): self
217
    {
218 3
        if ($attribute === '') {
219 1
            throw new RuntimeException('Attribute should not be empty string.');
220
        }
221
222 2
        $new = clone $this;
223 2
        $new->attributeIps = $attribute;
224 2
        return $new;
225
    }
226
227 18
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
228
    {
229
        /** @var string|null $actualHost */
230 18
        $actualHost = $request->getServerParams()['REMOTE_ADDR'] ?? null;
231
232 18
        if ($actualHost === null) {
233
            // Validation is not possible.
234 1
            return $this->handleNotTrusted($request, $handler);
235
        }
236
237 17
        $trustedHostData = null;
238 17
        $trustedHeaders = [];
239
240 17
        foreach ($this->trustedHosts as $data) {
241
            // collect all trusted headers
242 16
            $trustedHeaders[] = $data[self::DATA_KEY_TRUSTED_HEADERS];
243
244 16
            if ($trustedHostData !== null) {
245
                // trusted hosts already found
246 1
                continue;
247
            }
248
249 16
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS])) {
250 14
                $trustedHostData = $data;
251
            }
252
        }
253
254 17
        if ($trustedHostData === null) {
255
            // No trusted host at all.
256 3
            return $this->handleNotTrusted($request, $handler);
257
        }
258
259 14
        $trustedHeaders = array_merge(...$trustedHeaders);
260 14
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
261 14
        $request = $this->removeHeaders($request, $untrustedHeaders);
262
263 14
        [$ipListType, $ipHeader, $hostList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
264 14
        $hostList = array_reverse($hostList); // the first item should be the closest to the server
265
266 14
        if ($ipListType === self::IP_HEADER_TYPE_RFC7239) {
267 9
            $hostList = $this->getElementsByRfc7239($hostList);
268
        } else {
269 5
            $hostList = $this->getFormattedIpList($hostList);
270
        }
271
272 14
        array_unshift($hostList, ['ip' => $actualHost]); // server's ip to first position
273 14
        $hostDataList = [];
274
275
        do {
276 14
            $hostData = array_shift($hostList);
277 14
            if (!isset($hostData['ip'])) {
278
                $hostData = $this->reverseObfuscate($hostData, $hostDataList, $hostList, $request);
279
280
                if ($hostData === null) {
281
                    continue;
282
                }
283
284
                if (!isset($hostData['ip'])) {
285
                    break;
286
                }
287
            }
288
289 14
            $ip = $hostData['ip'];
290
291 14
            if (!$this->isValidHost($ip, ['any'])) {
292
                // invalid IP
293
                break;
294
            }
295
296 14
            $hostDataList[] = $hostData;
297
298 14
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS])) {
299
                // not trusted host
300 11
                break;
301
            }
302 14
        } while (count($hostList) > 0);
303
304 14
        if ($this->attributeIps !== null) {
305
            $request = $request->withAttribute($this->attributeIps, $hostDataList);
306
        }
307
308 14
        $uri = $request->getUri();
309
        // find HTTP host
310 14
        foreach ($trustedHostData[self::DATA_KEY_HOST_HEADERS] as $hostHeader) {
311 4
            if (!$request->hasHeader($hostHeader)) {
312
                continue;
313
            }
314
315
            if (
316 4
                $hostHeader === $ipHeader
317 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
318 4
                && isset($hostData['httpHost'])
319
            ) {
320 2
                $uri = $uri->withHost($hostData['httpHost']);
321 2
                break;
322
            }
323
324 2
            $host = $request->getHeaderLine($hostHeader);
325
326 2
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
327 2
                $uri = $uri->withHost($host);
328 2
                break;
329
            }
330
        }
331
332
        // find protocol
333
        /** @psalm-var ProtocolHeadersData $protocolHeadersData */
334 14
        $protocolHeadersData = $trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS];
335 14
        foreach ($protocolHeadersData as $protocolHeader => $protocols) {
336 4
            if (!$request->hasHeader($protocolHeader)) {
337
                continue;
338
            }
339
340
            if (
341 4
                $protocolHeader === $ipHeader
342 4
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
343 4
                && isset($hostData['protocol'])
344
            ) {
345 4
                $uri = $uri->withScheme($hostData['protocol']);
346 4
                break;
347
            }
348
349 2
            $protocolHeaderValue = $request->getHeaderLine($protocolHeader);
350
351 2
            foreach ($protocols as $protocol => $acceptedValues) {
352 2
                if (in_array($protocolHeaderValue, $acceptedValues, true)) {
353
                    $uri = $uri->withScheme($protocol);
354
                    break 2;
355
                }
356
            }
357
        }
358
359 14
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
360
361 14
        if ($urlParts !== null) {
362 3
            [$path, $query] = $urlParts;
363 3
            if ($path !== null) {
364 3
                $uri = $uri->withPath($path);
365
            }
366
367 3
            if ($query !== null) {
368 3
                $uri = $uri->withQuery($query);
369
            }
370
        }
371
372
        // find port
373 14
        foreach ($trustedHostData[self::DATA_KEY_PORT_HEADERS] as $portHeader) {
374 2
            if (!$request->hasHeader($portHeader)) {
375
                continue;
376
            }
377
378
            if (
379 2
                $portHeader === $ipHeader
380 2
                && $ipListType === self::IP_HEADER_TYPE_RFC7239
381 2
                && isset($hostData['port'])
382 2
                && $this->checkPort((string)$hostData['port'])
383
            ) {
384 1
                $uri = $uri->withPort((int)$hostData['port']);
385 1
                break;
386
            }
387
388 1
            $port = $request->getHeaderLine($portHeader);
389
390 1
            if ($this->checkPort($port)) {
391 1
                $uri = $uri->withPort((int)$port);
392 1
                break;
393
            }
394
        }
395
396 14
        return $handler->handle(
397 14
            $request->withUri($uri)->withAttribute(self::REQUEST_CLIENT_IP, $hostData['ip'] ?? null)
398 14
        );
399
    }
400
401
    /**
402
     * Validate host by range.
403
     *
404
     * This method can be extendable by overwriting e.g. with reverse DNS verification.
405
     *
406
     * @param string[] $ranges
407
     * @param Closure(string, string[]): Result $validator
408
     */
409 16
    protected function isValidHost(string $host, array $ranges): bool
410
    {
411 16
        $result = $this->validator->validate(
412 16
            $host,
413 16
            [new Ip(allowSubnet: false, allowNegation: false, ranges: $ranges)]
414 16
        );
415 16
        return $result->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 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...
427
     * @param array $hostDataListValidated
428
     * @param array $hostDataListRemaining
429
     * @param RequestInterface $request
430
     *
431
     * @return HostData|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
        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
444
    ): ?array {
445
        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...
446
    }
447
448 4
    private function handleNotTrusted(
449
        ServerRequestInterface $request,
450
        RequestHandlerInterface $handler
451
    ): ResponseInterface {
452 4
        if ($this->attributeIps !== null) {
453 1
            $request = $request->withAttribute($this->attributeIps, null);
454
        }
455
456 4
        return $handler->handle($request->withAttribute(self::REQUEST_CLIENT_IP, null));
457
    }
458
459
    /**
460
     * @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...
461
     */
462 25
    private function prepareProtocolHeaders(array $protocolHeaders): array
463
    {
464 25
        $output = [];
465
466 25
        foreach ($protocolHeaders as $header => $protocolAndAcceptedValues) {
467 8
            if (!is_string($header)) {
468
                throw new RuntimeException('The protocol header must be a string.');
469
            }
470 8
            $header = strtolower($header);
471
472 8
            if (is_callable($protocolAndAcceptedValues)) {
473 1
                $output[$header] = $protocolAndAcceptedValues;
474 1
                continue;
475
            }
476
477 7
            if (!is_array($protocolAndAcceptedValues)) {
478 1
                throw new RuntimeException('Accepted values is not an array nor callable.');
479
            }
480
481 6
            if ($protocolAndAcceptedValues === []) {
482 1
                throw new RuntimeException('Accepted values cannot be an empty array.');
483
            }
484
485 5
            $output[$header] = [];
486
487
            /**
488
             * @var array<string|string[]> $protocolAndAcceptedValues
489
             */
490 5
            foreach ($protocolAndAcceptedValues as $protocol => $acceptedValues) {
491 5
                if (!is_string($protocol)) {
492 1
                    throw new RuntimeException('The protocol must be a string.');
493
                }
494
495 4
                if ($protocol === '') {
496 1
                    throw new RuntimeException('The protocol cannot be empty.');
497
                }
498
499 3
                $output[$header][$protocol] = array_map('\strtolower', (array)$acceptedValues);
500
            }
501
        }
502
503 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...
504
    }
505
506
    /**
507
     * @param string[] $headers
508
     */
509 14
    private function removeHeaders(ServerRequestInterface $request, array $headers): ServerRequestInterface
510
    {
511 14
        foreach ($headers as $header) {
512
            $request = $request->withoutAttribute($header);
513
        }
514
515 14
        return $request;
516
    }
517
518
    /**
519
     * @param array<string|string[]> $ipHeaders
520
     *
521
     * @return array{0: string|null, 1: string|null, 2: string[]}
522
     */
523 14
    private function getIpList(ServerRequestInterface $request, array $ipHeaders): array
524
    {
525 14
        foreach ($ipHeaders as $ipHeader) {
526 12
            $type = null;
527
528 12
            if (is_array($ipHeader)) {
529 9
                $type = array_shift($ipHeader);
530 9
                $ipHeader = array_shift($ipHeader);
531
            }
532
533 12
            if ($request->hasHeader($ipHeader)) {
534 12
                return [$type, $ipHeader, $request->getHeader($ipHeader)];
535
            }
536
        }
537
538 2
        return [null, null, []];
539
    }
540
541
    /**
542
     * @param string[] $forwards
543
     *
544
     * @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...
545
     *
546
     * @see getElementsByRfc7239()
547
     */
548 5
    private function getFormattedIpList(array $forwards): array
549
    {
550 5
        $list = [];
551
552 5
        foreach ($forwards as $ip) {
553 3
            $list[] = ['ip' => $ip];
554
        }
555
556 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...
557
    }
558
559
    /**
560
     * Forwarded elements by RFC7239.
561
     *
562
     * The structure of the elements:
563
     * - `host`: IP or obfuscated hostname or "unknown"
564
     * - `ip`: IP address (only if presented)
565
     * - `by`: used user-agent by proxy (only if presented)
566
     * - `port`: port number received by proxy (only if presented)
567
     * - `protocol`: protocol received by proxy (only if presented)
568
     * - `httpHost`: HTTP host received by proxy (only if presented)
569
     *
570
     * The list starts with the server and the last item is the client itself.
571
     *
572
     * @link https://tools.ietf.org/html/rfc7239
573
     *
574
     * @param string[] $forwards
575
     *
576
     * @return list<HostData> Proxy data elements.
577
     */
578 9
    private function getElementsByRfc7239(array $forwards): array
579
    {
580 9
        $list = [];
581
582 9
        foreach ($forwards as $forward) {
583
            /** @var array<string, string> $data */
584 9
            $data = HeaderValueHelper::getParameters($forward);
585
586 9
            if (!isset($data['for'])) {
587
                // Invalid item, the following items will be dropped
588 2
                break;
589
            }
590
591 8
            $pattern = '/^(?<host>' . IpHelper::IPV4_PATTERN . '|unknown|_[\w\.-]+|[[]'
592 8
                . IpHelper::IPV6_PATTERN . '[]])(?::(?<port>[\w\.-]+))?$/';
593
594 8
            if (preg_match($pattern, $data['for'], $matches) === 0) {
595
                // Invalid item, the following items will be dropped
596 1
                break;
597
            }
598
599 8
            $ipData = [];
600 8
            $host = $matches['host'];
601 8
            $obfuscatedHost = $host === 'unknown' || str_starts_with($host, '_');
602
603 8
            if (!$obfuscatedHost) {
604
                // IPv4 & IPv6
605 8
                $ipData['ip'] = str_starts_with($host, '[') ? trim($host /* IPv6 */, '[]') : $host;
606
            }
607
608 8
            $ipData['host'] = $host;
609
610 8
            if (isset($matches['port'])) {
611 1
                $port = $matches['port'];
612
613 1
                if (!$obfuscatedHost && !$this->checkPort($port)) {
614
                    // Invalid port, the following items will be dropped
615
                    break;
616
                }
617
618 1
                $ipData['port'] = $obfuscatedHost ? $port : (int)$port;
619
            }
620
621
            // copy other properties
622 8
            foreach (['proto' => 'protocol', 'host' => 'httpHost', 'by' => 'by'] as $source => $destination) {
623 8
                if (isset($data[$source])) {
624 4
                    $ipData[$destination] = $data[$source];
625
                }
626
            }
627
628 8
            if (isset($ipData['httpHost']) && filter_var($ipData['httpHost'], FILTER_VALIDATE_DOMAIN) === false) {
629
                // remove not valid HTTP host
630
                unset($ipData['httpHost']);
631
            }
632
633 8
            $list[] = $ipData;
634
        }
635
636 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...
637
    }
638
639
    /**
640
     * @param string[] $urlHeaders
641
     *
642
     * @psalm-return non-empty-list<null|string>|null
643
     */
644 14
    private function getUrl(RequestInterface $request, array $urlHeaders): ?array
645
    {
646 14
        foreach ($urlHeaders as $header) {
647 3
            if (!$request->hasHeader($header)) {
648 1
                continue;
649
            }
650
651 3
            $url = $request->getHeaderLine($header);
652
653 3
            if (str_starts_with($url, '/')) {
654 3
                return array_pad(explode('?', $url, 2), 2, null);
655
            }
656
        }
657
658 11
        return null;
659
    }
660
661 2
    private function checkPort(string $port): bool
662
    {
663 2
        return preg_match('/^\d{1,5}$/', $port) === 1 && (int)$port <= 65535;
664
    }
665
666 21
    private function checkTypeStringOrArray(array $array, string $field): void
667
    {
668 21
        foreach ($array as $item) {
669 21
            if (!is_string($item)) {
670 1
                throw new InvalidArgumentException("$field must be string type");
671
            }
672
673 20
            if (trim($item) === '') {
674 2
                throw new InvalidArgumentException("$field cannot be empty strings");
675
            }
676
        }
677
    }
678
}
679