Passed
Push — master ( b45034...83eef8 )
by
unknown
03:50 queued 01:25
created

TrustedHeaderProtocolResolver::process()   B

Complexity

Conditions 11
Paths 14

Size

Total Lines 48
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 24
nc 14
nop 2
dl 0
loc 48
ccs 25
cts 25
cp 1
crap 11
rs 7.3166
c 0
b 0
f 0

How to fix   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 Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Server\MiddlewareInterface;
10
use Psr\Http\Server\RequestHandlerInterface;
11
use RuntimeException;
12
13
use function array_map;
14
use function in_array;
15
use function is_callable;
16
use function is_string;
17
use function strtolower;
18
19
/**
20
 * Trusted header protocol resolver is setting a server request protocol based on special header you trust
21
 * such as `X-Forwarded-Proto`.
22
 *
23
 * You can use it if your server is behind a trusted load balancer or a proxy that's always setting the special header
24
 * itself discarding any header values provided by user.
25
 *
26
 * If you want to trust headers for a certain IP, use {@see TrustedHostsNetworkResolver}.
27
 */
28
final class TrustedHeaderProtocolResolver implements MiddlewareInterface
29
{
30
    /**
31
     * Default mapping of protocol to header values.
32
     */
33
    private const DEFAULT_PROTOCOLS_TO_HEADER_VALUES = [
34
        'http' => ['http'],
35
        'https' => ['https', 'on'],
36
    ];
37
38
    /**
39
     * Lowercase trusted protocol headers and their corresponding mapping of protocols to header values.
40
     *
41
     * ```php
42
     * [
43
     *     'x-forwarded-proto' => [
44
     *         'http' => ['http'],
45
     *         'https' => ['https'],
46
     *     ],
47
     * ]
48
     * ```
49
     *
50
     * Instead of the mapping, it could be a callable.
51
     * See {@see withAddedProtocolHeader()}.
52
     *
53
     * @psalm-var array<string, non-empty-array<non-empty-string, non-empty-array<array-key, string>>|callable>
54
     */
55
    private array $protocolHeaders = [];
56
57
    /**
58
     * Returns a new instance with the specified protocol header added and protocols mapped to its values.
59
     * It's used to check whether the connection is HTTP / HTTPS or any other protocol.
60
     *
61
     * The match of header names and values is case-insensitive.
62
     * Avoid adding insecure/untrusted headers that a user might set.
63
     *
64
     * Accepted values:
65
     *
66
     * - `null` (default): Default mapping, see {@see DEFAULT_PROTOCOLS_TO_HEADER_VALUES}
67
     * - callable: custom function for getting the protocol
68
     * ```php
69
     * ->withProtocolHeader(
70
     *     'x-forwarded-proto',
71
     *     function (array $values, string $header, ServerRequestInterface $request): ?string {
72
     *         return $values[0] === 'https' ? 'https' : 'http';
73
     *         return null; // If you can not decide on the protocol.
74
     *     },
75
     * );
76
     * ```
77
     * - array: The array keys are protocols, and the array values are lists of header values that the header
78
     * must have for the corresponding protocol.
79
     *
80
     * ```php
81
     * ->withProtocolHeader('x-forwarded-proto', [
82
     *     'http' => ['http'],
83
     *     'https' => ['https'],
84
     * ]);
85
     * ```
86
     *
87
     * @param string $header The trusted protocol header name.
88
     * @param array|callable|null $values The protocol mapping to header values.
89
     *
90
     * @psalm-param array<array-key, string|string[]>|callable|null $values
91
     */
92 30
    public function withAddedProtocolHeader(string $header, array|callable|null $values = null): self
93
    {
94 30
        $new = clone $this;
95 30
        $header = strtolower($header);
96
97 30
        if ($values === null) {
98 5
            $new->protocolHeaders[$header] = self::DEFAULT_PROTOCOLS_TO_HEADER_VALUES;
99 5
            return $new;
100
        }
101
102 25
        if (is_callable($values)) {
103 11
            $new->protocolHeaders[$header] = $values;
104 11
            return $new;
105
        }
106
107 16
        if (empty($values)) {
108 1
            throw new RuntimeException('Protocol header values cannot be an empty array.');
109
        }
110
111 15
        $protocolHeader = [];
112 15
        foreach ($values as $protocol => $acceptedValues) {
113 15
            if (!is_string($protocol)) {
114 1
                throw new RuntimeException('The protocol must be type of string.');
115
            }
116
117 14
            if ($protocol === '') {
118 1
                throw new RuntimeException('The protocol cannot be an empty string.');
119
            }
120
121 13
            $acceptedValues = (array) $acceptedValues;
122 13
            if (empty($acceptedValues)) {
123 1
                throw new RuntimeException('Protocol accepted values cannot be an empty array.');
124
            }
125
126 12
            $protocolHeader[$protocol] = array_map('\strtolower', $acceptedValues);
127
        }
128
129 12
        $new->protocolHeaders[$header] = $protocolHeader;
130
131 12
        return $new;
132
    }
133
134
    /**
135
     * Returns a new instance without the specified protocol header.
136
     *
137
     * @param string $header The protocol header name.
138
     *
139
     * @see withAddedProtocolHeader()
140
     */
141 3
    public function withoutProtocolHeader(string $header): self
142
    {
143 3
        $new = clone $this;
144 3
        unset($new->protocolHeaders[strtolower($header)]);
145 3
        return $new;
146
    }
147
148
    /**
149
     * Returns a new instance without the specified protocol headers.
150
     *
151
     * @param string[] $headers The protocol header names. If you specify `null` all protocol headers will be removed.
152
     *
153
     * @see withoutProtocolHeader()
154
     */
155 3
    public function withoutProtocolHeaders(?array $headers = null): self
156
    {
157 3
        $new = clone $this;
158
159 3
        if ($headers === null) {
160 1
            $new->protocolHeaders = [];
161 1
            return $new;
162
        }
163
164 2
        foreach ($headers as $header) {
165 2
            $new = $new->withoutProtocolHeader($header);
166
        }
167
168 2
        return $new;
169
    }
170
171
    /**
172
     * {@inheritDoc}
173
     *
174
     * @throws RuntimeException If URI scheme protocol is wrong.
175
     */
176 27
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
177
    {
178 27
        $newScheme = null;
179
180 27
        foreach ($this->protocolHeaders as $header => $data) {
181 23
            if (!$request->hasHeader($header)) {
182 5
                continue;
183
            }
184
185 22
            $headerValues = $request->getHeader($header);
186
187 22
            if (is_callable($data)) {
188 11
                $newScheme = $data($headerValues, $header, $request);
189
190 11
                if ($newScheme === null) {
191 1
                    continue;
192
                }
193
194 10
                if (!is_string($newScheme)) {
195 8
                    throw new RuntimeException('The scheme is neither string nor null.');
196
                }
197
198 2
                if ($newScheme === '') {
199 1
                    throw new RuntimeException('The scheme cannot be an empty string.');
200
                }
201
202 1
                break;
203
            }
204
205 12
            $headerValue = strtolower($headerValues[0]);
206
207 12
            foreach ($data as $protocol => $acceptedValues) {
208 12
                if (!in_array($headerValue, $acceptedValues, true)) {
209 3
                    continue;
210
                }
211
212 10
                $newScheme = $protocol;
213 10
                break 2;
214
            }
215
        }
216
217 18
        $uri = $request->getUri();
218
219 18
        if ($newScheme !== null && $newScheme !== $uri->getScheme()) {
220 11
            $request = $request->withUri($uri->withScheme($newScheme));
221
        }
222
223 18
        return $handler->handle($request);
224
    }
225
}
226