Passed
Pull Request — master (#125)
by
unknown
06:23
created

BasicNetworkResolver::withAddedProtocolHeader()   B

Complexity

Conditions 8
Paths 8

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 8.4423

Importance

Changes 0
Metric Value
cc 8
eloc 20
nc 8
nop 2
dl 0
loc 29
ccs 17
cts 21
cp 0.8095
crap 8.4423
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
4
namespace Yiisoft\Yii\Web\Middleware;
5
6
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
12
/**
13
 * Basic network resolver
14
 *
15
 * It can be used in the following cases:
16
 * - not required IP resolve to access the user's IP
17
 * - user's IP is already resolved (eg `ngx_http_realip_module` or similar)
18
 *
19
 * @package Yiisoft\Yii\Web\Middleware
20
 */
21
class BasicNetworkResolver implements MiddlewareInterface
22
{
23
    private const DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES = [
24
        'http' => ['http'],
25
        'https' => ['https', 'on'],
26
    ];
27
28
    private $protocolHeaders = [];
29
30 14
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
31
    {
32 14
        $newScheme = null;
33 14
        foreach ($this->protocolHeaders as $header => $data) {
34 10
            if (!$request->hasHeader($header)) {
35 1
                continue;
36
            }
37 9
            $headerValues = $request->getHeader($header);
38 9
            if (is_callable($data)) {
39 2
                $newScheme = call_user_func($data, $headerValues, $header, $request);
40 2
                if ($newScheme === null) {
41 1
                    continue;
42
                }
43 1
                if (!is_string($newScheme)) {
44
                    throw new \RuntimeException('The scheme is neither string nor null!');
45
                }
46 1
                if (strlen($newScheme) === 0) {
47
                    throw new \RuntimeException('The scheme cannot be an empty string!');
48
                }
49 1
                break;
50
            }
51 7
            $headerValue = strtolower($headerValues[0]);
52 7
            foreach ($data as $protocol => $acceptedValues) {
53 7
                if (!in_array($headerValue, $acceptedValues)) {
54 2
                    continue;
55
                }
56 6
                $newScheme = $protocol;
57 6
                break 2;
58
            }
59
        }
60 14
        $uri = $request->getUri();
61 14
        if ($newScheme !== null && $newScheme !== $uri->getScheme()) {
62 7
            $request = $request->withUri($uri->withScheme($newScheme));
63
        }
64 14
        return $handler->handle($request);
65
    }
66
67
    /**
68
     * With added header to check for determining whether the connection is made via HTTP or HTTPS (or any protocol).
69
     *
70
     * The match of header names and values is case-insensitive.
71
     * It's not advisable to put insecure/untrusted headers here.
72
     *
73
     * Accepted types of values:
74
     * - NULL (default): {{DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES}}
75
     * - callable: custom function for getting the protocol
76
     * ```php
77
     * ->withProtocolHeader('x-forwarded-proto', function(array $values, string $header, ServerRequestInterface $request) {
78
     *   return $values[0] === 'https' ? 'https' : 'http';
79
     *   return null;     // If it doesn't make sense.
80
     * });
81
     * ```
82
     * - array: The array keys are protocol string and the array value is a list of header values that indicate the protocol.
83
     * ```php
84
     * ->withProtocolHeader('x-forwarded-proto', [
85
     *   'http' => ['http'],
86
     *   'https' => ['https']
87
     * ]);
88
     * ```
89
     *
90
     * @param callable|array|null $values
91
     * @return static
92
     * @see DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES
93
     */
94 12
    public function withAddedProtocolHeader(string $header, $values = null)
95
    {
96 12
        $new = clone $this;
97 12
        $header = strtolower($header);
98 12
        if ($values === null) {
99 4
            $new->protocolHeaders[$header] = self::DEFAULT_PROTOCOL_AND_ACCEPTABLE_VALUES;
100 4
            return $new;
101
        }
102 8
        if (is_callable($values)) {
103 2
            $new->protocolHeaders[$header] = $values;
104 2
            return $new;
105
        }
106 6
        if (!is_array($values)) {
107
            throw new \RuntimeException('Accepted values is not array nor callable!');
108
        }
109 6
        if (count($values) === 0) {
110
            throw new \RuntimeException('Accepted values cannot be an empty array!');
111
        }
112 6
        $new->protocolHeaders[$header] = [];
113 6
        foreach ($values as $protocol => $acceptedValues) {
114 6
            if (!is_string($protocol)) {
115
                throw new \RuntimeException('The protocol must be type of string!');
116
            }
117 6
            if (strlen($protocol) === 0) {
118
                throw new \RuntimeException('The protocol cannot be an empty string!');
119
            }
120 6
            $new->protocolHeaders[$header][$protocol] = array_map('strtolower', (array)$acceptedValues);
121
        }
122 6
        return $new;
123
    }
124
125
    /**
126
     * @return static
127
     */
128 2
    public function withoutProtocolHeader(string $header)
129
    {
130 2
        $new = clone $this;
131 2
        unset($new->protocolHeaders[strtolower($header)]);
132 2
        return $new;
133
    }
134
135
    /**
136
     * @return static
137
     */
138 2
    public function withoutProtocolHeaders(?array $headers = null)
139
    {
140 2
        $new = clone $this;
141 2
        if ($headers === null) {
142 1
            $new->protocolHeaders = [];
143
        } else {
144 1
            foreach ($headers as $header) {
145 1
                $new = $new->withoutProtocolHeader($header);
146
            }
147
        }
148 2
        return $new;
149
    }
150
}
151