xref: /dokuwiki/inc/Ip.php (revision 25a70af9b4a17fa21228193868c69038088160af)
1<?php
2
3/**
4 * DokuWiki IP address functions.
5 *
6 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author     Zebra North <mrzebra@mrzebra.co.uk>
8 */
9
10namespace dokuwiki;
11
12use dokuwiki\Input\Input;
13use Exception;
14
15class Ip
16{
17    /**
18     * Determine whether an IP address is within a given CIDR range.
19     * The needle and haystack may be either IPv4 or IPv6.
20     *
21     * Example:
22     *
23     * ipInRange('192.168.11.123', '192.168.0.0/16') === true
24     * ipInRange('192.168.11.123', '::192.168.0.0/80') === true
25     * ipInRange('::192.168.11.123', '192.168.0.0/16') === true
26     * ipInRange('::192.168.11.123', '::192.168.0.0/80') === true
27     *
28     * @param string $needle The IP to test, either IPv4 in dotted decimal
29     *                         notation or IPv6 in colon notation.
30     * @param string $haystack The CIDR range as an IP followed by a forward
31     *                         slash and the number of significant bits.
32     *
33     * @return bool Returns true if $needle is within the range specified
34     *              by $haystack, false if it is outside the range.
35     *
36     * @throws Exception Thrown if $needle is not a valid IP address.
37     * @throws Exception Thrown if $haystack is not a valid IP range.
38     */
39    public static function ipInRange(string $needle, string $haystack): bool
40    {
41        $range = explode('/', $haystack);
42        $networkIp = Ip::ipToNumber($range[0]);
43        $maskLength = $range[1];
44
45        // For an IPv4 address the top 96 bits must be zero.
46        if ($networkIp['version'] === 4) {
47            $maskLength += 96;
48        }
49
50        if ($maskLength > 128) {
51            throw new Exception('Invalid IP range mask: ' . $haystack);
52        }
53
54
55        $needle = Ip::ipToNumber($needle);
56
57        $maskLengthUpper = min($maskLength, 64);
58        $maskLengthLower = max(0, $maskLength - 64);
59
60        if (PHP_INT_SIZE == 4) {
61            $needle_up = Ip::bitmask64_32($needle['upper'],    $maskLengthUpper);
62            $net_up    = Ip::bitmask64_32($networkIp['upper'], $maskLengthUpper);
63            $needle_lo = Ip::bitmask64_32($needle['lower'],    $maskLengthLower);
64            $net_lo    = Ip::bitmask64_32($networkIp['lower'], $maskLengthLower);
65        } else {
66            $maskUpper = ~0 << intval(64 - $maskLengthUpper);
67            $maskLower = ~0 << intval(64 - $maskLengthLower);
68
69            $needle_up = $needle['upper'] & $maskUpper;
70            $net_up    = $networkIp['upper'] & $maskUpper;
71            $needle_lo = $needle['lower'] & $maskLower;
72            $net_lo    = $networkIp['lower'] & $maskLower;
73        }
74
75        return $needle_up === $net_up && $needle_lo === $net_lo;
76    }
77
78    /**
79     * modeling bitshift like  ~0 << $pow for 32-bit arch
80     * @param pow power of 2 for mask
81     * @return 64-char string of 1 and 0s
82     * pow=1
83     * 1111111111111111111111111111111111111111111111111111111111111110
84     * pow=63
85     * 1000000000000000000000000000000000000000000000000000000000000000
86     * pow=64
87     * 0000000000000000000000000000000000000000000000000000000000000000
88     */
89    private static function make_bitmask_32(int $pow) : string {
90        $pow = $pow < 0 ? 64 - $pow : $pow;
91        $mask = sprintf("%064d",0);
92        for ($i=0; $i<64; $i++) {
93            if ($i >= $pow) {
94                $mask[63 - $i] = '1';
95            }
96        }
97        return $mask;
98    }
99    /**
100     * slow and ugly bitwise_and for 32bit arch
101     * @param $u64 unsigned 64bit integer as string
102     *            likely from ipv6_upper_lower_32
103     * @param $pow 0-64 power of 2 for bitmask
104     */
105    private static function bitmask64_32(string $u64, int $pow) : string {
106        //$u64 = sprintf("%.0f", $u65);
107        $b32 = '4294967296';
108        $bin = sprintf("%032b%032b",
109                bcdiv($u64, $b32, 0),
110                bcmod($u64, $b32));
111
112        $mask = Ip::make_bitmask_32(64-$pow);
113
114        // most right is lowest bit
115        $res='0';
116        for ($i=0; $i<64; $i++){
117            if (bcmul($bin[$i], $mask[$i]) == 1) {
118                $res = bcadd($res, bcpow(2, 63-$i));
119            }
120        }
121        return $res;
122    }
123
124    /**
125     * Convert an IP address from a string to a number.
126     *
127     * This splits 128 bit IP addresses into the upper and lower 64 bits, and
128     * also returns whether the IP given was IPv4 or IPv6.
129     *
130     * The returned array contains:
131     *
132     *  - version: Either '4' or '6'.
133     *  - upper: The upper 64 bits of the IP.
134     *  - lower: The lower 64 bits of the IP.
135     *
136     * For an IPv4 address, 'upper' will always be zero.
137     *
138     * @param string $ip The IPv4 or IPv6 address.
139     *
140     * @return int[] Returns an array of 'version', 'upper', 'lower'.
141     *
142     * @throws Exception Thrown if the IP is not valid.
143     */
144    public static function ipToNumber(string $ip): array
145    {
146        $binary = inet_pton($ip);
147
148        if ($binary === false) {
149            throw new Exception('Invalid IP: ' . $ip);
150        }
151
152        if (strlen($binary) === 4) {
153            // IPv4.
154            return [
155                'version' => 4,
156                'upper' => 0,
157                'lower' => unpack('Nip', $binary)['ip'],
158            ];
159        } else {
160            // IPv6. strlen==16
161            if(PHP_INT_SIZE == 8) { // 64-bit arch
162               $result = unpack('Jupper/Jlower', $binary);
163            } else { // 32-bit
164               $result = Ip::ipv6_upper_lower_32($binary);
165            }
166            $result['version'] = 6;
167            return $result;
168        }
169    }
170
171    /**
172     * conversion of inet_pton ipv6 into 64-bit upper and lower
173     * bcmath version for 32-bit architecture
174     * w/o no unpack('J') - unsigned long long (always 64 bit, big endian byte order)
175     *
176     * results match unpack('Jupper/Jlower', $binary)
177     *
178     * @param string $binary inet_pton's ipv6 16 element binary
179     *
180     * @return int[] upper 64 and lower 64 for ipToNumber
181     */
182    public static function ipv6_upper_lower_32(string $binary) {
183       // unpack into four 32-bit unsigned ints to recombine as 2 64-bit
184       $b32 = 4294967296; // bcpow(2, 32)
185       $parts = unpack('N4', $binary);
186       $upper = bcadd(bcmul($parts[1], $b32),
187                      $parts[2]);
188       $lower = bcadd(bcmul($parts[3], $b32),
189                      $parts[4]);
190       // ISSUE:
191       // unpack('J2') on 64bit is stored as 2 signed int (even if J is unsigned)
192       // here upper and lower have to be strings. numbers wont fit in 32-bit
193       return ['upper' => $upper, 'lower' => $lower];
194    }
195
196    /**
197     * Determine if an IP address is equal to another IP or within an IP range.
198     * IPv4 and IPv6 are supported.
199     *
200     * @param string $ip The address to test.
201     * @param string $ipOrRange An IP address or CIDR range.
202     *
203     * @return bool Returns true if the IP matches, false if not.
204     */
205    public static function ipMatches(string $ip, string $ipOrRange): bool
206    {
207        try {
208            // If it's not a range, compare the addresses directly.
209            // Addresses are converted to numbers because the same address may be
210            // represented by different strings, e.g. "::1" and "::0001".
211            if (strpos($ipOrRange, '/') === false) {
212                return Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange);
213            }
214
215            return Ip::ipInRange($ip, $ipOrRange);
216        } catch (Exception $ex) {
217            // The IP address was invalid.
218            return false;
219        }
220    }
221
222    /**
223     * Given the IP address of a proxy server, determine whether it is
224     * a known and trusted server.
225     *
226     * This test is performed using the config value `trustedproxies`.
227     *
228     * @param string $ip The IP address of the proxy.
229     *
230     * @return bool Returns true if the IP is trusted as a proxy.
231     */
232    public static function proxyIsTrusted(string $ip): bool
233    {
234        global $conf;
235
236        // If the configuration is empty then no proxies are trusted.
237        if (empty($conf['trustedproxies'])) {
238            return false;
239        }
240
241        foreach ((array)$conf['trustedproxies'] as $trusted) {
242            if (Ip::ipMatches($ip, $trusted)) {
243                return true; // The given IP matches one of the trusted proxies.
244            }
245        }
246
247        return false; // none of the proxies matched
248    }
249
250    /**
251     * Get the originating IP address and the address of every proxy that the
252     * request has passed through, according to the X-Forwarded-For header.
253     *
254     * To prevent spoofing of the client IP, every proxy listed in the
255     * X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint
256     * from which the connection was received (i.e. the final proxy).
257     *
258     * If the header is not present or contains an untrusted proxy then
259     * an empty array is returned.
260     *
261     * The client IP is the first entry in the returned list, followed by the
262     * proxies.
263     *
264     * @return string[] Returns an array of IP addresses.
265     */
266    public static function forwardedFor(): array
267    {
268        /* @var Input $INPUT */
269        global $INPUT, $conf;
270
271        $forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR');
272
273        if (empty($conf['trustedproxies']) || !$forwardedFor) {
274            return [];
275        }
276
277        // This is the address from which the header was received.
278        $remoteAddr = $INPUT->server->str('REMOTE_ADDR');
279
280        // Get the client address from the X-Forwarded-For header.
281        // X-Forwarded-For: <client> [, <proxy>]...
282        $forwardedFor = explode(',', str_replace(' ', '', $forwardedFor));
283
284        // The client address is the first item, remove it from the list.
285        $clientAddress = array_shift($forwardedFor);
286
287        // The remaining items are the proxies through which the X-Forwarded-For
288        // header has passed.  The final proxy is the connection's remote address.
289        $proxies = $forwardedFor;
290        $proxies[] = $remoteAddr;
291
292        // Ensure that every proxy is trusted.
293        foreach ($proxies as $proxy) {
294            if (!Ip::proxyIsTrusted($proxy)) {
295                return [];
296            }
297        }
298
299        // Add the client address before the list of proxies.
300        return array_merge([$clientAddress], $proxies);
301    }
302
303    /**
304     * Return the IP of the client.
305     *
306     * The IP is sourced from, in order of preference:
307     *
308     *   - The X-Real-IP header if $conf[realip] is true.
309     *   - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy].
310     *   - The TCP/IP connection remote address.
311     *   - 0.0.0.0 if all else fails.
312     *
313     * The 'realip' config value should only be set to true if the X-Real-IP header
314     * is being added by the web server, otherwise it may be spoofed by the client.
315     *
316     * The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For
317     * may be spoofed by the client.
318     *
319     * @return string Returns an IPv4 or IPv6 address.
320     */
321    public static function clientIp(): string
322    {
323        return Ip::clientIps()[0];
324    }
325
326    /**
327     * Return the IP of the client and the proxies through which the connection has passed.
328     *
329     * The IPs are sourced from, in order of preference:
330     *
331     *   - The X-Real-IP header if $conf[realip] is true.
332     *   - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
333     *   - The TCP/IP connection remote address.
334     *   - 0.0.0.0 if all else fails.
335     *
336     * @return string[] Returns an array of IPv4 or IPv6 addresses.
337     */
338    public static function clientIps(): array
339    {
340        /* @var Input $INPUT */
341        global $INPUT, $conf;
342
343        // IPs in order of most to least preferred.
344        $ips = [];
345
346        // Use the X-Real-IP header if it is enabled by the configuration.
347        if (!empty($conf['realip']) && $INPUT->server->str('HTTP_X_REAL_IP')) {
348            $ips[] = $INPUT->server->str('HTTP_X_REAL_IP');
349        }
350
351        // Add the X-Forwarded-For addresses if all proxies are trusted.
352        $ips = array_merge($ips, Ip::forwardedFor());
353
354        // Add the TCP/IP connection endpoint.
355        $ips[] = $INPUT->server->str('REMOTE_ADDR');
356
357        // Remove invalid IPs.
358        $ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP));
359
360        // Remove duplicated IPs.
361        $ips = array_values(array_unique($ips));
362
363        // Add a fallback if for some reason there were no valid IPs.
364        if (!$ips) {
365            $ips[] = '0.0.0.0';
366        }
367
368        return $ips;
369    }
370}
371