Passed
Pull Request — master (#42)
by Rustam
02:40
created

TrustedHostsNetworkResolver::handleNotTrusted()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

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

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

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