Completed
Pull Request — master (#157)
by
unknown
02:00
created

withAddedTrustedHosts()   C

Complexity

Conditions 12
Paths 26

Size

Total Lines 55
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 12
eloc 34
c 3
b 0
f 0
nc 26
nop 6
dl 0
loc 55
rs 6.9666

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
namespace Yiisoft\Yii\Web\Middleware;
4
5
use Psr\Http\Message\RequestInterface;
6
use Psr\Http\Message\ResponseFactoryInterface;
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Server\MiddlewareInterface;
10
use Psr\Http\Server\RequestHandlerInterface;
11
use Yiisoft\NetworkUtilities\IpHelper;
12
use Yiisoft\Validator\Rule\Ip;
13
use Yiisoft\Yii\Web\Helper\HeaderHelper;
14
15
class TrustedHostsNetworkResolver implements MiddlewareInterface
16
{
17
    public const IP_HEADER_TYPE_RFC7239 = 'rfc7239';
18
19
    private const DEFAULT_IP_HEADERS = [
20
        [self::IP_HEADER_TYPE_RFC7239, 'forward'], // https://tools.ietf.org/html/rfc7239
21
        'x-forwarded-for', // common
22
    ];
23
24
    private const DEFAULT_HOST_HEADERS = [
25
        'x-forwarded-host', // common
26
    ];
27
28
    private const DEFAULT_URL_HEADERS = [
29
        'x-rewrite-url', // Microsoft
30
    ];
31
32
    private const DEFAULT_PROTOCOL_HEADERS = [
33
        'x-forwarded-proto' => ['http' => 'http', 'https' => 'https'], // common
34
        'front-end-https' => ['https' => 'on'], // Microsoft
35
    ];
36
37
    private const DEFAULT_TRUSTED_HEADERS = [
38
        // common:
39
        'x-forwarded-for',
40
        'x-forwarded-host',
41
        'x-forwarded-proto',
42
43
        // RFC:
44
        'forward',
45
46
        // Microsoft:
47
        'front-end-https',
48
        'x-rewrite-url',
49
    ];
50
51
    private const DATA_KEY_HOSTS = 'hosts';
52
    private const DATA_KEY_IP_HEADERS = 'ipHeaders';
53
    private const DATA_KEY_HOST_HEADERS = 'hostHeaders';
54
    private const DATA_KEY_URL_HEADERS = 'urlHeaders';
55
    private const DATA_KEY_PROTOCOL_HEADERS = 'protocolHeaders';
56
    private const DATA_KEY_TRUSTED_HEADERS = 'trustedHeaders';
57
58
    private $trustedHosts = [];
59
60
    /**
61
     * @var string|null
62
     */
63
    private $attributeIps;
64
65
    /**
66
     * @var ResponseFactoryInterface
67
     */
68
    private $responseFactory;
69
    /**
70
     * @var Chain|null
71
     */
72
    private $notTrustedBranch;
73
74
    /**
75
     * @var Ip|null
76
     */
77
    private $ipValidator;
78
79
    public function __construct(ResponseFactoryInterface $responseFactory)
80
    {
81
        $this->responseFactory = $responseFactory;
82
    }
83
84
    /**
85
     * @return static
86
     */
87
    public function withIpValidator(Ip $ipValidator)
88
    {
89
        $new = clone $this;
90
        $ipValidator = clone $ipValidator;
91
        // force disable unacceptable validation
92
        $new->ipValidator = $ipValidator->disallowSubnet()->disallowNegation();
93
        return $new;
94
    }
95
96
    /**
97
     * @return static
98
     */
99
    public function withNotTrustedBranch(?MiddlewareInterface $middleware)
100
    {
101
        $new = clone $this;
102
        $new->notTrustedBranch = $middleware;
103
        return $new;
104
    }
105
106
    /**
107
     * @return static
108
     */
109
    public function withAddedTrustedHosts(
110
        array $hosts,
111
        ?array $ipHeaders = null,
112
        ?array $protocolHeaders = null,
113
        ?array $hostHeaders = null,
114
        ?array $urlHeaders = null,
115
        ?array $trustedHeaders = null
116
    ) {
117
        $new = clone $this;
118
        $ipHeaders = $ipHeaders ?? self::DEFAULT_IP_HEADERS;
119
        foreach ($ipHeaders as $ipHeader) {
120
            if (\is_string($ipHeader)) {
121
                continue;
122
            }
123
            if (!\is_array($ipHeader)) {
124
                throw new \InvalidArgumentException('Type of ipHeader is not a string and not array');
125
            }
126
            if (count($ipHeader) !== 2) {
127
                throw new \InvalidArgumentException('The ipHeader array must have exactly 2 elements');
128
            }
129
            [$type, $header] = $ipHeader;
130
            if (!\is_string($type)) {
131
                throw new \InvalidArgumentException('The type is not a string');
132
            }
133
            if (!\is_string($header)) {
134
                throw new \InvalidArgumentException('The header is not a string');
135
            }
136
            if ($type === self::IP_HEADER_TYPE_RFC7239) {
137
                continue;
138
            }
139
140
            throw new \InvalidArgumentException("Not supported IP header type: $type");
141
        }
142
        if (count($hosts) === 0) {
143
            throw new \InvalidArgumentException("Empty hosts not allowed");
144
        }
145
        $data = [
146
            self::DATA_KEY_HOSTS => $hosts,
147
            self::DATA_KEY_IP_HEADERS => $ipHeaders,
148
            self::DATA_KEY_PROTOCOL_HEADERS => $this->prepareProtocolHeaders($protocolHeaders ?? self::DEFAULT_PROTOCOL_HEADERS),
149
            self::DATA_KEY_TRUSTED_HEADERS => $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS,
150
            self::DATA_KEY_HOST_HEADERS => $hostHeaders ?? self::DEFAULT_HOST_HEADERS,
151
            self::DATA_KEY_URL_HEADERS => $urlHeaders ?? self::DEFAULT_URL_HEADERS,
152
        ];
153
        foreach ([self::DATA_KEY_HOSTS, self::DATA_KEY_TRUSTED_HEADERS, self::DATA_KEY_HOST_HEADERS, self::DATA_KEY_URL_HEADERS] as $key) {
154
            $this->checkStringArrayType($data[$key], $key);
155
        }
156
        foreach ($data[self::DATA_KEY_HOSTS] as $host) {
157
            $host = str_replace('*', 'wildcard', $host);        // wildcard is allowed in host
158
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) === false) {
159
                throw new \InvalidArgumentException("'$host' host is not a domain and not an IP address");
160
            }
161
        }
162
        $new->trustedHosts[] = $data;
163
        return $new;
164
    }
165
166
    private function checkStringArrayType(array $array, string $field): void
167
    {
168
        foreach ($array as $item) {
169
            if (!is_string($item)) {
170
                throw new \InvalidArgumentException("$field must be string type");
171
            }
172
            if (trim($item) === '') {
173
                throw new \InvalidArgumentException("$field cannot be empty strings");
174
            }
175
        }
176
    }
177
178
    /**
179
     * @return static
180
     */
181
    public function withoutTrustedHosts()
182
    {
183
        $new = clone $this;
184
        $new->trustedHosts = [];
185
        return $new;
186
    }
187
188
    /**
189
     * @return static
190
     */
191
    public function withAttributeIps(?string $attribute)
192
    {
193
        if ($attribute === '') {
194
            throw new \RuntimeException('Attribute should not be empty');
195
        }
196
        $new = clone $this;
197
        $new->attributeIps = $attribute;
198
        return $new;
199
    }
200
201
    /**
202
     * Process an incoming server request.
203
     *
204
     * Processes an incoming server request in order to produce a response.
205
     * If unable to produce the response itself, it may delegate to the provided
206
     * request handler to do so.
207
     */
208
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
209
    {
210
        $actualHost = $request->getServerParams()['REMOTE_ADDR'];
211
        $trustedHostData = null;
212
        $trustedHeaders = [];
213
        $ipValidator = $this->ipValidator ?? new Ip();
214
        foreach ($this->trustedHosts as $data) {
215
            // collect all trusted headers
216
            $trustedHeaders = array_merge($trustedHeaders, $data[self::DATA_KEY_TRUSTED_HEADERS]);
217
            if ($trustedHostData !== null) {
218
                // trusted hosts already found
219
                continue;
220
            }
221
            if ($this->isValidHost($actualHost, $data[self::DATA_KEY_HOSTS], $ipValidator)) {
222
                $trustedHostData = $data;
223
            }
224
        }
225
        $untrustedHeaders = array_diff($trustedHeaders, $trustedHostData[self::DATA_KEY_TRUSTED_HEADERS] ?? []);
226
        $request = $this->removeHeaders($request, $untrustedHeaders);
227
        if ($trustedHostData === null) {
228
            // No trusted host at all.
229
            if ($this->notTrustedBranch !== null) {
230
                return $this->notTrustedBranch->process($request, $handler);
231
            }
232
            $response = $this->responseFactory->createResponse(412);
233
            $response->getBody()->write('Unable to verify your network.');
234
            return $response;
235
        }
236
        [$type, $ipList] = $this->getIpList($request, $trustedHostData[self::DATA_KEY_IP_HEADERS]);
237
        $ipList = array_reverse($ipList);       // the first item should be the closest to the server
238
        if ($type === null) {
239
            $ipList = $this->getFormattedIpList($ipList);
240
        } elseif ($type === self::IP_HEADER_TYPE_RFC7239) {
241
            $ipList = $this->getForwardedElements($ipList);
242
        }
243
        array_unshift($ipList, ['ip' => $actualHost]);  // server's ip to first position
244
        $ipDataList = [];
245
        do {
246
            $ipData = array_shift($ipList);
247
            if (!isset($ipData['ip'])) {
248
                $ipData = $this->reverseObfuscate($ipData, $ipDataList, $ipList, $request);
249
                if (!isset($ipData['ip'])) {
250
                    break;
251
                }
252
            }
253
            $ip = $ipData['ip'];
254
            if (!$this->isValidHost($ip, ['any'], $ipValidator)) {
255
                break;
256
            }
257
            $ipDataList[] = $ipData;
258
            if (!$this->isValidHost($ip, $trustedHostData[self::DATA_KEY_HOSTS], $ipValidator)) {
259
                break;
260
            }
261
        } while (count($ipList) > 0);
262
263
        if ($this->attributeIps !== null) {
264
            $request = $request->withAttribute($this->attributeIps, $ipDataList);
265
        }
266
267
        $uri = $request->getUri();
268
        if (isset($ipData['httpHost'])) {
269
            $uri = $uri->withHost($ipData['httpHost']);
270
        } else {
271
            // find host from headers
272
            $host = $this->getHttpHost($request, $trustedHostData[self::DATA_KEY_HOST_HEADERS]);
273
            if ($host !== null) {
274
                $uri = $uri->withHost($host);
275
            }
276
        }
277
        if (isset($ipData['protocol'])) {
278
            $uri = $uri->withScheme($ipData['protocol']);
279
        } else {
280
            // find scheme from headers
281
            $scheme = $this->getScheme($request, $trustedHostData[self::DATA_KEY_PROTOCOL_HEADERS]);
282
            if ($scheme !== null) {
283
                $uri = $uri->withScheme($scheme);
284
            }
285
        }
286
        $urlParts = $this->getUrl($request, $trustedHostData[self::DATA_KEY_URL_HEADERS]);
287
        if ($urlParts !== null) {
288
            [$path, $query] = $urlParts;
289
            $uri = $uri->withPath($path);
290
            if ($query !== null) {
291
                $uri = $uri->withQuery($query);
292
            }
293
        }
294
        return $handler->handle($request->withUri($uri)->withAttribute('requestClientIp', $ipData['ip']));
295
    }
296
297
    /**
298
     * Validate host by range
299
     *
300
     * This method can be extendable by overwriting eg. with reverse DNS verification.
301
     */
302
    protected function isValidHost(string $host, array $ranges, Ip $validator): bool
303
    {
304
        return $validator->ranges($ranges)->validate($host)->isValid();
305
    }
306
307
    /**
308
     * Reverse obfuscating host data
309
     *
310
     * The base operation does not perform any transformation on the data.
311
     * This method can be extendable by overwriting eg.
312
     */
313
    protected function reverseObfuscate(
314
        array $ipData,
315
        array $ipDataListValidated,
0 ignored issues
show
Unused Code introduced by
The parameter $ipDataListValidated 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

315
        /** @scrutinizer ignore-unused */ array $ipDataListValidated,

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...
316
        array $ipDataListRemaining,
0 ignored issues
show
Unused Code introduced by
The parameter $ipDataListRemaining 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

316
        /** @scrutinizer ignore-unused */ array $ipDataListRemaining,

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...
317
        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

317
        /** @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...
318
    ): array {
319
        return $ipData;
320
    }
321
322
    private function prepareProtocolHeaders(array $protocolHeaders): array
323
    {
324
        $output = [];
325
        foreach ($protocolHeaders as $header => $protocolAndAcceptedValues) {
326
            $header = strtolower($header);
327
            if (\is_callable($protocolAndAcceptedValues)) {
328
                $output[$header] = $protocolAndAcceptedValues;
329
                continue;
330
            }
331
            if (!\is_array($protocolAndAcceptedValues)) {
332
                throw new \RuntimeException('Accepted values is not an array nor callable');
333
            }
334
            if (count($protocolAndAcceptedValues) === 0) {
335
                throw new \RuntimeException('Accepted values cannot be an empty array');
336
            }
337
            $output[$header] = [];
338
            foreach ($protocolAndAcceptedValues as $protocol => $acceptedValues) {
339
                if (!\is_string($protocol)) {
340
                    throw new \RuntimeException('The protocol must be a string');
341
                }
342
                if ($protocol === '') {
343
                    throw new \RuntimeException('The protocol cannot be empty');
344
                }
345
                $output[$header][$protocol] = array_map('strtolower', (array)$acceptedValues);
346
            }
347
        }
348
        return $output;
349
    }
350
351
    private function removeHeaders(ServerRequestInterface $request, array $headers): ServerRequestInterface
352
    {
353
        foreach ($headers as $header) {
354
            $request = $request->withoutAttribute($header);
355
        }
356
        return $request;
357
    }
358
359
    private function getIpList(RequestInterface $request, array $ipHeaders): array
360
    {
361
        foreach ($ipHeaders as $ipHeader) {
362
            $type = null;
363
            if (\is_array($ipHeader)) {
364
                $type = array_shift($ipHeader);
365
                $ipHeader = array_shift($ipHeader);
366
            }
367
            if ($request->hasHeader($ipHeader)) {
368
                return [$type, $request->getHeader($ipHeader)];
369
            }
370
        }
371
        return [null, []];
372
    }
373
374
    private function getFormattedIpList(array $forwards): array
375
    {
376
        $list = [];
377
        foreach ($forwards as $ip) {
378
            $list[] = ['ip' => $ip];
379
        }
380
        return $list;
381
    }
382
383
    /**
384
     * Forwarded elements by RFC7239
385
     *
386
     * @link https://tools.ietf.org/html/rfc7239
387
     */
388
    private function getForwardedElements(array $forwards): array
389
    {
390
        $list = [];
391
        foreach ($forwards as $forward) {
392
            $data = HeaderHelper::getParameters($forward);
393
            if (!isset($data['for'])) {
394
                // Invalid item, the following items will be dropped
395
                break;
396
            }
397
            $pattern = '/^(?<host>' . IpHelper::IPV4_PATTERN . '|unknown|_[\w\.-]+|[[]' . IpHelper::IPV6_PATTERN . '[]])(?::(?<port>[\w\.-]+))?$/';
398
            if (preg_match($pattern, $data['for'], $matches) === 0) {
399
                // Invalid item, the following items will be dropped
400
                break;
401
            }
402
            $ipData = [];
403
            $host = $matches['host'];
404
            $obfuscatedHost = $host === 'unknown' || strpos($host, '_') === 0;
405
            if (!$obfuscatedHost) {
406
                // IPv4 & IPv6
407
                $ipData['ip'] = strpos($host, '[') === 0 ? trim($host /* IPv6 */, '[]') : $host;
408
            }
409
            $ipData['host'] = $host;
410
            if (isset($matches['port'])) {
411
                $port = $matches['port'];
412
                if (!$obfuscatedHost && (preg_match('/^\d{1,5}$/', $port) === 0 || (int)$port > 65535)) {
413
                    // Invalid port, the following items will be dropped
414
                    break;
415
                }
416
                $ipData['port'] = $obfuscatedHost ? $port : (int)$port;
417
            }
418
419
            // copy other properties
420
            foreach (['proto' => 'protocol', 'host' => 'httpHost', 'by' => 'by'] as $source => $destination) {
421
                if (isset($data[$source])) {
422
                    $ipData[$destination] = $data[$source];
423
                }
424
            }
425
426
            $list[] = $ipData;
427
        }
428
        return $list;
429
    }
430
431
    private function getHttpHost(RequestInterface $request, array $hostHeaders): ?string
432
    {
433
        foreach ($hostHeaders as $header) {
434
            if (!$request->hasHeader($header)) {
435
                continue;
436
            }
437
            $host = $request->getHeaderLine($header);
438
            if (filter_var($host, FILTER_VALIDATE_DOMAIN) !== false) {
439
                return $host;
440
            }
441
        }
442
        return null;
443
    }
444
445
    private function getScheme(RequestInterface $request, array $protocolHeaders): ?string
446
    {
447
        foreach ($protocolHeaders as $header => $ref) {
448
            if (!$request->hasHeader($header)) {
449
                continue;
450
            }
451
            $value = strtolower($request->getHeaderLine($header));
452
            foreach ($ref as $protocol => $acceptedValues) {
453
                if (\in_array($value, $acceptedValues, true)) {
454
                    return $protocol;
455
                }
456
            }
457
        }
458
        return null;
459
    }
460
461
    private function getUrl(RequestInterface $request, array $urlHeaders): ?array
462
    {
463
        foreach ($urlHeaders as $header) {
464
            if (!$request->hasHeader($header)) {
465
                continue;
466
            }
467
            $url = $request->getHeaderLine($header);
468
            if (strpos($url, '/') === 0) {
469
                return array_pad(explode('?', $url, 2), 2, null);
470
            }
471
        }
472
        return null;
473
    }
474
}
475