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