BasicNetworkResolver::withoutProtocolHeader()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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