Completed
Pull Request — master (#125)
by
unknown
02:14
created

prepareProtocolHeaders()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 8
eloc 15
c 1
b 0
f 1
nc 7
nop 1
dl 0
loc 21
rs 8.4444
ccs 0
cts 21
cp 0
crap 72
1
<?php
2
3
4
namespace Yiisoft\Yii\Web\NetworkResolver;
5
6
7
use Psr\Http\Message\ServerRequestInterface;
8
use Yiisoft\Validator\Rule\Ip;
9
10
class TrustedHostsNetworkResolver implements NetworkResolverInterface
11
{
12
13
    private const DEFAULT_IP_HEADERS = [
14
        'X-Forwarded-For', // Common
15
    ];
16
17
    private const DEFAULT_PROTOCOL_HEADERS = [
18
        'X-Forwarded-Proto' => ['http' => 'http', 'https' => 'https'], // Common
19
        'Front-End-Https' => ['https' => 'on'], // Microsoft
20
    ];
21
22
    private const DEFAULT_TRUSTED_HEADERS = [
23
        // Common:
24
        'X-Forwarded-For',
25
        'X-Forwarded-Host',
26
        'X-Forwarded-Proto',
27
28
        // Microsoft:
29
        'Front-End-Https',
30
        'X-Rewrite-Url',
31
    ];
32
33
    private $trustedHosts = [];
34
35
    /**
36
     * @var ServerRequestInterface|null
37
     */
38
    private $baseServerRequest;
39
40
    /**
41
     * @var int
42
     */
43
    private $remoteIpIndex = 0;
0 ignored issues
show
introduced by
The private property $remoteIpIndex is not used, and could be removed.
Loading history...
44
45
    /**
46
     * @var array
47
     */
48
    private $cacheIpList = [];
49
    private $cacheIsTrusted = false;
50
    /**
51
     * @var ServerRequestInterface|null
52
     */
53
    private $cacheServerRequest;
54
55
    /**
56
     * @return static
57
     */
58
    public function withNewTrustedHosts(
59
        array $hosts,
60
        ?array $ipHeaders = null,
61
        ?array $protocolHeaders = null,
62
        ?array $trustedHeaders = null
63
    ) {
64
        $new = clone $this;
65
        $new->trustedHosts[] = [
66
            'hosts' => $hosts,
67
            'ipHeaders' => $ipHeaders ?? self::DEFAULT_IP_HEADERS,
68
            'protocolHeaders' => $this->prepareProtocolHeaders($protocolHeaders ?? self::DEFAULT_PROTOCOL_HEADERS),
69
            'trustedHeaders' => $trustedHeaders ?? self::DEFAULT_TRUSTED_HEADERS,
70
        ];
71
        return $new;
72
    }
73
74
    /**
75
     * @return static
76
     */
77
    public function withoutTrustedHosts()
78
    {
79
        $new = clone $this;
80
        $new->trustedHosts = [];
81
        return $new;
82
    }
83
84
    protected function prepareProtocolHeaders(array $protocolHeaders): array
85
    {
86
        $output = [];
87
        foreach ($protocolHeaders as $header => $protocolAndAcceptedValues) {
88
            if (is_callable($protocolAndAcceptedValues)) {
89
                $output[$header] = $protocolAndAcceptedValues;
90
            } elseif (!is_array($protocolAndAcceptedValues)) {
91
                throw new \RuntimeException('$protocolAndAcceptedValues is not array nor callable!');
92
            } elseif (is_array($protocolAndAcceptedValues) && count($protocolAndAcceptedValues) === 0) {
93
                throw new \RuntimeException('$protocolAndAcceptedValues cannot be an empty array!');
94
            } else {
95
                $output[$header] = [];
96
                foreach ($protocolAndAcceptedValues as $protocol => $acceptedValues) {
97
                    if (!is_string($protocol)) {
98
                        throw new \RuntimeException('The protocol must be type of string!');
99
                    }
100
                    $output[$header][$protocol] = array_map('strtolower', (array)$acceptedValues);
101
                }
102
            }
103
        }
104
        return $output;
105
    }
106
107
    /**
108
     * @return static
109
     */
110
    public function withServerRequest(ServerRequestInterface $serverRequest)
111
    {
112
        $new = clone $this;
113
        $new->baseServerRequest = $serverRequest;
114
        return $new;
115
    }
116
117
    public function getRemoteIp(): string
118
    {
119
        $this->getServerRequest();
120
        return $this->cacheIpList[0];
121
    }
122
123
    public function getUserIp(): string
124
    {
125
        $this->getServerRequest();
126
        return end($this->cacheIpList);
127
    }
128
129
    /**
130
     * Security of user's connection
131
     */
132
    public function isSecureConnection(): bool
133
    {
134
        return $this->getServerRequest()->getUri()->getScheme() === 'https';
135
    }
136
137
    public function __clone()
138
    {
139
        $this->cacheIpList = [];
140
        $this->cacheIsTrusted = false;
141
        $this->cacheServerRequest = null;
142
    }
143
144
    protected function getBaseServerRequest(bool $throwIfNull = true): ?ServerRequestInterface
145
    {
146
        if ($this->baseServerRequest === null && $throwIfNull) {
147
            throw new \RuntimeException('The server request object is not set!');
148
        }
149
        return $this->baseServerRequest;
150
    }
151
152
    public function getServerRequest(): ServerRequestInterface
153
    {
154
        if ($this->cacheServerRequest !== null) {
155
            return $this->cacheServerRequest;
156
        }
157
158
        $request = $this->getBaseServerRequest();
159
        $actualHost = $request->getServerParams()['REMOTE_ADDR'];
160
        $this->cacheIpList = [$actualHost];
161
        $trustedHostData = null;
162
        $trustedHeadersMerge = [];
163
        foreach ($this->trustedHosts as $data) {
164
            $trustedHeadersMerge = array_merge($trustedHeadersMerge, $data['trustedHeaders']);
165
            if ($trustedHostData !== null) {
166
                continue;
167
            } elseif (!$this->isValidHost($actualHost, $data['hosts'])) {
168
                continue;
169
            }
170
            $trustedHostData = $data;
171
        }
172
        if ($trustedHostData === null) {
173
            // No trusted host at all.
174
            return $this->cacheServerRequest = $this->removeHeaders($request, $trustedHeadersMerge);
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type null; however, parameter $request of Yiisoft\Yii\Web\NetworkR...solver::removeHeaders() does only seem to accept Psr\Http\Message\ServerRequestInterface, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
            return $this->cacheServerRequest = $this->removeHeaders(/** @scrutinizer ignore-type */ $request, $trustedHeadersMerge);
Loading history...
175
        }
176
177
        $request = $this->removeHeaders($request, array_diff($trustedHeadersMerge, $trustedHostData['trustedHeaders']));
178
179
        $ipList = null;
180
        foreach ($trustedHostData['ipHeaders'] as $ipHeader) {
181
            if ($request->hasHeader($ipHeader)) {
182
                $ipList = $request->getHeader($ipHeader)[0];
183
                break;
184
            }
185
        }
186
187
        if ($ipList !== null) {
188
            $ips = preg_split('/\s*,\s*/', trim($ipList), -1, PREG_SPLIT_NO_EMPTY);
189
            while (count($ips)) {
190
                $ip = array_pop($ips);
191
                if($this->isValidHost($ip, ['any'])) {
192
                    $this->cacheIpList[] = $ip;
193
                }
194
                if (!$this->isValidHost($ip, $trustedHostData['hosts'])) {
195
                    break;
196
                }
197
            }
198
        }
199
200
        return $this->cacheServerRequest = $request;
201
    }
202
203
    protected function removeHeaders(ServerRequestInterface $request, array $headers): ServerRequestInterface
204
    {
205
        foreach ($headers as $header) {
206
            $request = $request->withoutAttribute($header);
207
        }
208
        return $request;
209
    }
210
211
    /**
212
     * Validate host by range
213
     *
214
     * This method can be extendable by overwriting eg. with reverse DNS verification.
215
     *
216
     * @param string   $host
217
     * @param string[] $ranges
218
     */
219
    protected function isValidHost(string $host, array $ranges): bool
220
    {
221
// @TODO Ip validator not working
222
//        if($ranges == ['any']) {
223
//            return true;
224
//        }
225
//        return $host == $ranges[0];
226
        $validator = new Ip();
227
        $validator->setRanges($ranges);
228
        return $validator->validateValue($host)->isValid();
229
    }
230
}
231