1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
|
<?php
/**
* DokuWiki IP address functions.
*
* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
* @author Zebra North <mrzebra@mrzebra.co.uk>
*/
namespace dokuwiki;
use dokuwiki\Input\Input;
use Exception;
class Ip
{
/**
* Determine whether an IP address is within a given CIDR range.
* The needle and haystack may be either IPv4 or IPv6.
*
* Example:
*
* ipInRange('192.168.11.123', '192.168.0.0/16') === true
* ipInRange('192.168.11.123', '::192.168.0.0/80') === true
* ipInRange('::192.168.11.123', '192.168.0.0/16') === true
* ipInRange('::192.168.11.123', '::192.168.0.0/80') === true
*
* @param string $needle The IP to test, either IPv4 in dotted decimal
* notation or IPv6 in colon notation.
* @param string $haystack The CIDR range as an IP followed by a forward
* slash and the number of significant bits.
*
* @return bool Returns true if $needle is within the range specified
* by $haystack, false if it is outside the range.
*
* @throws Exception Thrown if $needle is not a valid IP address.
* @throws Exception Thrown if $haystack is not a valid IP range.
*/
public static function ipInRange(string $needle, string $haystack): bool
{
$range = explode('/', $haystack);
$networkIp = Ip::ipToNumber($range[0]);
$maskLength = $range[1];
// For an IPv4 address the top 96 bits must be zero.
if ($networkIp['version'] === 4) {
$maskLength += 96;
}
if ($maskLength > 128) {
throw new Exception('Invalid IP range mask: ' . $haystack);
}
$maskLengthUpper = min($maskLength, 64);
$maskLengthLower = max(0, $maskLength - 64);
$maskUpper = ~0 << intval(64 - $maskLengthUpper);
$maskLower = ~0 << intval(64 - $maskLengthLower);
$needle = Ip::ipToNumber($needle);
return ($needle['upper'] & $maskUpper) === ($networkIp['upper'] & $maskUpper) &&
($needle['lower'] & $maskLower) === ($networkIp['lower'] & $maskLower);
}
/**
* Convert an IP address from a string to a number.
*
* This splits 128 bit IP addresses into the upper and lower 64 bits, and
* also returns whether the IP given was IPv4 or IPv6.
*
* The returned array contains:
*
* - version: Either '4' or '6'.
* - upper: The upper 64 bits of the IP.
* - lower: The lower 64 bits of the IP.
*
* For an IPv4 address, 'upper' will always be zero.
*
* @param string $ip The IPv4 or IPv6 address.
*
* @return int[] Returns an array of 'version', 'upper', 'lower'.
*
* @throws Exception Thrown if the IP is not valid.
*/
public static function ipToNumber(string $ip): array
{
$binary = inet_pton($ip);
if ($binary === false) {
throw new Exception('Invalid IP: ' . $ip);
}
if (strlen($binary) === 4) {
// IPv4.
return [
'version' => 4,
'upper' => 0,
'lower' => unpack('Nip', $binary)['ip'],
];
} else {
// IPv6.
$result = unpack('Jupper/Jlower', $binary);
$result['version'] = 6;
return $result;
}
}
/**
* Determine if an IP address is equal to another IP or within an IP range.
* IPv4 and IPv6 are supported.
*
* @param string $ip The address to test.
* @param string $ipOrRange An IP address or CIDR range.
*
* @return bool Returns true if the IP matches, false if not.
*/
public static function ipMatches(string $ip, string $ipOrRange): bool
{
try {
// If it's not a range, compare the addresses directly.
// Addresses are converted to numbers because the same address may be
// represented by different strings, e.g. "::1" and "::0001".
if (strpos($ipOrRange, '/') === false) {
return Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange);
}
return Ip::ipInRange($ip, $ipOrRange);
} catch (Exception $ex) {
// The IP address was invalid.
return false;
}
}
/**
* Given the IP address of a proxy server, determine whether it is
* a known and trusted server.
*
* This test is performed using the config value `trustedproxies`.
*
* @param string $ip The IP address of the proxy.
*
* @return bool Returns true if the IP is trusted as a proxy.
*/
public static function proxyIsTrusted(string $ip): bool
{
global $conf;
// If the configuration is empty then no proxies are trusted.
if (empty($conf['trustedproxies'])) {
return false;
}
foreach ((array)$conf['trustedproxies'] as $trusted) {
if (Ip::ipMatches($ip, $trusted)) {
return true; // The given IP matches one of the trusted proxies.
}
}
return false; // none of the proxies matched
}
/**
* Get the originating IP address and the address of every proxy that the
* request has passed through, according to the X-Forwarded-For header.
*
* To prevent spoofing of the client IP, every proxy listed in the
* X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint
* from which the connection was received (i.e. the final proxy).
*
* If the header is not present or contains an untrusted proxy then
* an empty array is returned.
*
* The client IP is the first entry in the returned list, followed by the
* proxies.
*
* @return string[] Returns an array of IP addresses.
*/
public static function forwardedFor(): array
{
/* @var Input $INPUT */
global $INPUT, $conf;
$forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR');
if (empty($conf['trustedproxies']) || !$forwardedFor) {
return [];
}
// This is the address from which the header was received.
$remoteAddr = $INPUT->server->str('REMOTE_ADDR');
// Get the client address from the X-Forwarded-For header.
// X-Forwarded-For: <client> [, <proxy>]...
$forwardedFor = explode(',', str_replace(' ', '', $forwardedFor));
// The client address is the first item, remove it from the list.
$clientAddress = array_shift($forwardedFor);
// The remaining items are the proxies through which the X-Forwarded-For
// header has passed. The final proxy is the connection's remote address.
$proxies = $forwardedFor;
$proxies[] = $remoteAddr;
// Ensure that every proxy is trusted.
foreach ($proxies as $proxy) {
if (!Ip::proxyIsTrusted($proxy)) {
return [];
}
}
// Add the client address before the list of proxies.
return array_merge([$clientAddress], $proxies);
}
/**
* Return the IP of the client.
*
* The IP is sourced from, in order of preference:
*
* - The X-Real-IP header if $conf[realip] is true.
* - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy].
* - The TCP/IP connection remote address.
* - 0.0.0.0 if all else fails.
*
* The 'realip' config value should only be set to true if the X-Real-IP header
* is being added by the web server, otherwise it may be spoofed by the client.
*
* The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For
* may be spoofed by the client.
*
* @return string Returns an IPv4 or IPv6 address.
*/
public static function clientIp(): string
{
return Ip::clientIps()[0];
}
/**
* Return the IP of the client and the proxies through which the connection has passed.
*
* The IPs are sourced from, in order of preference:
*
* - The X-Real-IP header if $conf[realip] is true.
* - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
* - The TCP/IP connection remote address.
* - 0.0.0.0 if all else fails.
*
* @return string[] Returns an array of IPv4 or IPv6 addresses.
*/
public static function clientIps(): array
{
/* @var Input $INPUT */
global $INPUT, $conf;
// IPs in order of most to least preferred.
$ips = [];
// Use the X-Real-IP header if it is enabled by the configuration.
if (!empty($conf['realip']) && $INPUT->server->str('HTTP_X_REAL_IP')) {
$ips[] = $INPUT->server->str('HTTP_X_REAL_IP');
}
// Add the X-Forwarded-For addresses if all proxies are trusted.
$ips = array_merge($ips, Ip::forwardedFor());
// Add the TCP/IP connection endpoint.
$ips[] = $INPUT->server->str('REMOTE_ADDR');
// Remove invalid IPs.
$ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP));
// Remove duplicated IPs.
$ips = array_values(array_unique($ips));
// Add a fallback if for some reason there were no valid IPs.
if (!$ips) {
$ips[] = '0.0.0.0';
}
return $ips;
}
}
|