xref: /dokuwiki/inc/Ip.php (revision 40981bcc82357fbbd1b690d99568e7e7a022a5c0)
1c7f6b7b7SZebra North<?php
2c7f6b7b7SZebra North
3c7f6b7b7SZebra North/**
433cb4e01SAndreas Gohr * DokuWiki IP address and reverse proxy 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;
1391da8d44SWillForanuse dokuwiki\Ip32;
14c7f6b7b7SZebra Northuse Exception;
15c7f6b7b7SZebra North
16c7f6b7b7SZebra Northclass Ip
17c7f6b7b7SZebra North{
18c7f6b7b7SZebra North    /**
19c7f6b7b7SZebra North     * Determine whether an IP address is within a given CIDR range.
20c7f6b7b7SZebra North     * The needle and haystack may be either IPv4 or IPv6.
21c7f6b7b7SZebra North     *
22c7f6b7b7SZebra North     * Example:
23c7f6b7b7SZebra North     *
24c7f6b7b7SZebra North     * ipInRange('192.168.11.123', '192.168.0.0/16') === true
25c7f6b7b7SZebra North     * ipInRange('192.168.11.123', '::192.168.0.0/80') === true
26c7f6b7b7SZebra North     * ipInRange('::192.168.11.123', '192.168.0.0/16') === true
27c7f6b7b7SZebra North     * ipInRange('::192.168.11.123', '::192.168.0.0/80') === true
28c7f6b7b7SZebra North     *
29e449acd0SAndreas Gohr     * @param string $needle The IP to test, either IPv4 in dotted decimal
30c7f6b7b7SZebra North     *                         notation or IPv6 in colon notation.
31c7f6b7b7SZebra North     * @param string $haystack The CIDR range as an IP followed by a forward
32c7f6b7b7SZebra North     *                         slash and the number of significant bits.
33c7f6b7b7SZebra North     *
34c7f6b7b7SZebra North     * @return bool Returns true if $needle is within the range specified
35c7f6b7b7SZebra North     *              by $haystack, false if it is outside the range.
36c7f6b7b7SZebra North     *
37c7f6b7b7SZebra North     * @throws Exception Thrown if $needle is not a valid IP address.
38c7f6b7b7SZebra North     * @throws Exception Thrown if $haystack is not a valid IP range.
39c7f6b7b7SZebra North     */
40c7f6b7b7SZebra North    public static function ipInRange(string $needle, string $haystack): bool
41c7f6b7b7SZebra North    {
42c7f6b7b7SZebra North        $range = explode('/', $haystack);
43c7f6b7b7SZebra North        $networkIp = Ip::ipToNumber($range[0]);
44*40981bccSAndreas Gohr
45*40981bccSAndreas Gohr        // The mask length must be a non-negative integer.
46*40981bccSAndreas Gohr        if (!isset($range[1]) || !ctype_digit($range[1])) {
47*40981bccSAndreas Gohr            throw new Exception('Invalid IP range mask: ' . $haystack);
48*40981bccSAndreas Gohr        }
49*40981bccSAndreas Gohr        $maskLength = (int) $range[1];
50c7f6b7b7SZebra North
51c7f6b7b7SZebra North        // For an IPv4 address the top 96 bits must be zero.
52c7f6b7b7SZebra North        if ($networkIp['version'] === 4) {
53c7f6b7b7SZebra North            $maskLength += 96;
54c7f6b7b7SZebra North        }
55c7f6b7b7SZebra North
56c7f6b7b7SZebra North        if ($maskLength > 128) {
57c7f6b7b7SZebra North            throw new Exception('Invalid IP range mask: ' . $haystack);
58c7f6b7b7SZebra North        }
59c7f6b7b7SZebra North
60c7f6b7b7SZebra North
61c7f6b7b7SZebra North        $needle = Ip::ipToNumber($needle);
62c7f6b7b7SZebra North
6325a70af9SWillForan        $maskLengthUpper = min($maskLength, 64);
6425a70af9SWillForan        $maskLengthLower = max(0, $maskLength - 64);
6525a70af9SWillForan
6625a70af9SWillForan        if (PHP_INT_SIZE == 4) {
67a060f5a0SWillForan            $needle_up = Ip32::bitmask64On32($needle['upper'], $maskLengthUpper);
68a060f5a0SWillForan            $net_up    = Ip32::bitmask64On32($networkIp['upper'], $maskLengthUpper);
69a060f5a0SWillForan            $needle_lo = Ip32::bitmask64On32($needle['lower'], $maskLengthLower);
70a060f5a0SWillForan            $net_lo    = Ip32::bitmask64On32($networkIp['lower'], $maskLengthLower);
7125a70af9SWillForan        } else {
7225a70af9SWillForan            $maskUpper = ~0 << intval(64 - $maskLengthUpper);
7325a70af9SWillForan            $maskLower = ~0 << intval(64 - $maskLengthLower);
7425a70af9SWillForan
7525a70af9SWillForan            $needle_up = $needle['upper'] & $maskUpper;
7625a70af9SWillForan            $net_up    = $networkIp['upper'] & $maskUpper;
7725a70af9SWillForan            $needle_lo = $needle['lower'] & $maskLower;
7825a70af9SWillForan            $net_lo    = $networkIp['lower'] & $maskLower;
7925a70af9SWillForan        }
8025a70af9SWillForan
8125a70af9SWillForan        return $needle_up === $net_up && $needle_lo === $net_lo;
82c7f6b7b7SZebra North    }
83c7f6b7b7SZebra North
84c7f6b7b7SZebra North    /**
85c7f6b7b7SZebra North     * Convert an IP address from a string to a number.
86c7f6b7b7SZebra North     *
87c7f6b7b7SZebra North     * This splits 128 bit IP addresses into the upper and lower 64 bits, and
88c7f6b7b7SZebra North     * also returns whether the IP given was IPv4 or IPv6.
89c7f6b7b7SZebra North     *
90c7f6b7b7SZebra North     * The returned array contains:
91c7f6b7b7SZebra North     *
92c7f6b7b7SZebra North     *  - version: Either '4' or '6'.
93c7f6b7b7SZebra North     *  - upper: The upper 64 bits of the IP.
94c7f6b7b7SZebra North     *  - lower: The lower 64 bits of the IP.
95c7f6b7b7SZebra North     *
96c7f6b7b7SZebra North     * For an IPv4 address, 'upper' will always be zero.
97c7f6b7b7SZebra North     *
98e449acd0SAndreas Gohr     * @param string $ip The IPv4 or IPv6 address.
99c7f6b7b7SZebra North     *
100c7f6b7b7SZebra North     * @return int[] Returns an array of 'version', 'upper', 'lower'.
101c7f6b7b7SZebra North     *
102c7f6b7b7SZebra North     * @throws Exception Thrown if the IP is not valid.
103c7f6b7b7SZebra North     */
104c7f6b7b7SZebra North    public static function ipToNumber(string $ip): array
105c7f6b7b7SZebra North    {
106c7f6b7b7SZebra North        $binary = inet_pton($ip);
107c7f6b7b7SZebra North
108c7f6b7b7SZebra North        if ($binary === false) {
109c7f6b7b7SZebra North            throw new Exception('Invalid IP: ' . $ip);
110c7f6b7b7SZebra North        }
111c7f6b7b7SZebra North
112c7f6b7b7SZebra North        if (strlen($binary) === 4) {
113c7f6b7b7SZebra North            // IPv4.
1142f70db90SWillForan            $ipNum = unpack('Nip', $binary)['ip'];
1152f70db90SWillForan            if (PHP_INT_SIZE == 4) {
1162f70db90SWillForan                // integer overlfow on 32bit: negative even though 'N'=unsigned
1172f70db90SWillForan                $ipNum = ($ipNum < 0) ? bcadd($ipNum, Ip32::$b32) : (string)$ipNum;
1182f70db90SWillForan            }
119c7f6b7b7SZebra North            return [
120c7f6b7b7SZebra North                'version' => 4,
121c7f6b7b7SZebra North                'upper' => 0,
1222f70db90SWillForan                'lower' => $ipNum,
123c7f6b7b7SZebra North            ];
124c7f6b7b7SZebra North        } else {
125da569c7fSWillForan            // IPv6. strlen==16
1262f70db90SWillForan            if (PHP_INT_SIZE == 4) { // 32-bit
127a060f5a0SWillForan                $result = Ip32::ipv6UpperLowerOn32($binary);
1282f70db90SWillForan            } else { // 64-bit arch
129c7f6b7b7SZebra North                $result = unpack('Jupper/Jlower', $binary);
130c9e618caSWillForan            }
131c7f6b7b7SZebra North            $result['version'] = 6;
132c7f6b7b7SZebra North            return $result;
133c7f6b7b7SZebra North        }
134c7f6b7b7SZebra North    }
135c7f6b7b7SZebra North
136c7f6b7b7SZebra North    /**
137c7f6b7b7SZebra North     * Determine if an IP address is equal to another IP or within an IP range.
138c7f6b7b7SZebra North     * IPv4 and IPv6 are supported.
139c7f6b7b7SZebra North     *
140c7f6b7b7SZebra North     * @param string $ip The address to test.
141c7f6b7b7SZebra North     * @param string $ipOrRange An IP address or CIDR range.
142c7f6b7b7SZebra North     *
143c7f6b7b7SZebra North     * @return bool Returns true if the IP matches, false if not.
144c7f6b7b7SZebra North     */
145c7f6b7b7SZebra North    public static function ipMatches(string $ip, string $ipOrRange): bool
146c7f6b7b7SZebra North    {
147c7f6b7b7SZebra North        try {
148c7f6b7b7SZebra North            // If it's not a range, compare the addresses directly.
149c7f6b7b7SZebra North            // Addresses are converted to numbers because the same address may be
150c7f6b7b7SZebra North            // represented by different strings, e.g. "::1" and "::0001".
151093fe67eSAndreas Gohr            if (!str_contains($ipOrRange, '/')) {
152c7f6b7b7SZebra North                return Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange);
153c7f6b7b7SZebra North            }
154c7f6b7b7SZebra North
155c7f6b7b7SZebra North            return Ip::ipInRange($ip, $ipOrRange);
156*40981bccSAndreas Gohr        } catch (\Throwable) {
157*40981bccSAndreas Gohr            // The IP address or range was invalid.
158c7f6b7b7SZebra North            return false;
159c7f6b7b7SZebra North        }
160c7f6b7b7SZebra North    }
161c7f6b7b7SZebra North
162c7f6b7b7SZebra North    /**
163c7f6b7b7SZebra North     * Given the IP address of a proxy server, determine whether it is
164c7f6b7b7SZebra North     * a known and trusted server.
165c7f6b7b7SZebra North     *
16619d5ba27SAndreas Gohr     * This test is performed using the config value `trustedproxies`.
167c7f6b7b7SZebra North     *
168c7f6b7b7SZebra North     * @param string $ip The IP address of the proxy.
169c7f6b7b7SZebra North     *
170c7f6b7b7SZebra North     * @return bool Returns true if the IP is trusted as a proxy.
171c7f6b7b7SZebra North     */
172c7f6b7b7SZebra North    public static function proxyIsTrusted(string $ip): bool
173c7f6b7b7SZebra North    {
174c7f6b7b7SZebra North        global $conf;
175c7f6b7b7SZebra North
176c7f6b7b7SZebra North        // If the configuration is empty then no proxies are trusted.
17719d5ba27SAndreas Gohr        if (empty($conf['trustedproxies'])) {
178c7f6b7b7SZebra North            return false;
179c7f6b7b7SZebra North        }
180c7f6b7b7SZebra North
18119d5ba27SAndreas Gohr        foreach ((array)$conf['trustedproxies'] as $trusted) {
182c7f6b7b7SZebra North            if (Ip::ipMatches($ip, $trusted)) {
183ced0b55fSAndreas Gohr                return true; // The given IP matches one of the trusted proxies.
184c7f6b7b7SZebra North            }
185c7f6b7b7SZebra North        }
186c7f6b7b7SZebra North
187ced0b55fSAndreas Gohr        return false; // none of the proxies matched
188c7f6b7b7SZebra North    }
189c7f6b7b7SZebra North
190c7f6b7b7SZebra North    /**
191c7f6b7b7SZebra North     * Get the originating IP address and the address of every proxy that the
192c7f6b7b7SZebra North     * request has passed through, according to the X-Forwarded-For header.
193c7f6b7b7SZebra North     *
194c7f6b7b7SZebra North     * To prevent spoofing of the client IP, every proxy listed in the
195c7f6b7b7SZebra North     * X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint
196c7f6b7b7SZebra North     * from which the connection was received (i.e. the final proxy).
197c7f6b7b7SZebra North     *
198c7f6b7b7SZebra North     * If the header is not present or contains an untrusted proxy then
199c7f6b7b7SZebra North     * an empty array is returned.
200c7f6b7b7SZebra North     *
201c7f6b7b7SZebra North     * The client IP is the first entry in the returned list, followed by the
202c7f6b7b7SZebra North     * proxies.
203c7f6b7b7SZebra North     *
204c7f6b7b7SZebra North     * @return string[] Returns an array of IP addresses.
205c7f6b7b7SZebra North     */
206c7f6b7b7SZebra North    public static function forwardedFor(): array
207c7f6b7b7SZebra North    {
208c7f6b7b7SZebra North        /* @var Input $INPUT */
209c7f6b7b7SZebra North        global $INPUT, $conf;
210c7f6b7b7SZebra North
211c7f6b7b7SZebra North        $forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR');
212c7f6b7b7SZebra North
213ced0b55fSAndreas Gohr        if (empty($conf['trustedproxies']) || !$forwardedFor) {
214c7f6b7b7SZebra North            return [];
215c7f6b7b7SZebra North        }
216c7f6b7b7SZebra North
217c7f6b7b7SZebra North        // This is the address from which the header was received.
218c7f6b7b7SZebra North        $remoteAddr = $INPUT->server->str('REMOTE_ADDR');
219c7f6b7b7SZebra North
220c7f6b7b7SZebra North        // Get the client address from the X-Forwarded-For header.
221c7f6b7b7SZebra North        // X-Forwarded-For: <client> [, <proxy>]...
222c7f6b7b7SZebra North        $forwardedFor = explode(',', str_replace(' ', '', $forwardedFor));
223c7f6b7b7SZebra North
224c7f6b7b7SZebra North        // The client address is the first item, remove it from the list.
225c7f6b7b7SZebra North        $clientAddress = array_shift($forwardedFor);
226c7f6b7b7SZebra North
227c7f6b7b7SZebra North        // The remaining items are the proxies through which the X-Forwarded-For
228c7f6b7b7SZebra North        // header has passed.  The final proxy is the connection's remote address.
229c7f6b7b7SZebra North        $proxies = $forwardedFor;
230c7f6b7b7SZebra North        $proxies[] = $remoteAddr;
231c7f6b7b7SZebra North
232c7f6b7b7SZebra North        // Ensure that every proxy is trusted.
233c7f6b7b7SZebra North        foreach ($proxies as $proxy) {
234c7f6b7b7SZebra North            if (!Ip::proxyIsTrusted($proxy)) {
235c7f6b7b7SZebra North                return [];
236c7f6b7b7SZebra North            }
237c7f6b7b7SZebra North        }
238c7f6b7b7SZebra North
239c7f6b7b7SZebra North        // Add the client address before the list of proxies.
240c7f6b7b7SZebra North        return array_merge([$clientAddress], $proxies);
241c7f6b7b7SZebra North    }
242c7f6b7b7SZebra North
243c7f6b7b7SZebra North    /**
244c7f6b7b7SZebra North     * Return the IP of the client.
245c7f6b7b7SZebra North     *
246c7f6b7b7SZebra North     * The IP is sourced from, in order of preference:
247c7f6b7b7SZebra North     *
24890c2f6e3SAndreas Gohr     *   - The custom IP header if $conf[client_ip_header] is set.
249c7f6b7b7SZebra North     *   - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy].
250c7f6b7b7SZebra North     *   - The TCP/IP connection remote address.
251c7f6b7b7SZebra North     *   - 0.0.0.0 if all else fails.
252c7f6b7b7SZebra North     *
25390c2f6e3SAndreas Gohr     * The 'client_ip_header' config value should only be set if the header
254c7f6b7b7SZebra North     * is being added by the web server, otherwise it may be spoofed by the client.
255c7f6b7b7SZebra North     *
256c7f6b7b7SZebra North     * The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For
257c7f6b7b7SZebra North     * may be spoofed by the client.
258c7f6b7b7SZebra North     *
259c7f6b7b7SZebra North     * @return string Returns an IPv4 or IPv6 address.
260c7f6b7b7SZebra North     */
261c7f6b7b7SZebra North    public static function clientIp(): string
262c7f6b7b7SZebra North    {
263c7f6b7b7SZebra North        return Ip::clientIps()[0];
264c7f6b7b7SZebra North    }
265c7f6b7b7SZebra North
266c7f6b7b7SZebra North    /**
267c7f6b7b7SZebra North     * Return the IP of the client and the proxies through which the connection has passed.
268c7f6b7b7SZebra North     *
269c7f6b7b7SZebra North     * The IPs are sourced from, in order of preference:
270c7f6b7b7SZebra North     *
27190c2f6e3SAndreas Gohr     *   - The custom IP header if $conf[client_ip_header] is set.
27219d5ba27SAndreas Gohr     *   - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
273c7f6b7b7SZebra North     *   - The TCP/IP connection remote address.
274c7f6b7b7SZebra North     *   - 0.0.0.0 if all else fails.
275c7f6b7b7SZebra North     *
276c7f6b7b7SZebra North     * @return string[] Returns an array of IPv4 or IPv6 addresses.
277c7f6b7b7SZebra North     */
278c7f6b7b7SZebra North    public static function clientIps(): array
279c7f6b7b7SZebra North    {
280c7f6b7b7SZebra North        /* @var Input $INPUT */
281c7f6b7b7SZebra North        global $INPUT, $conf;
282c7f6b7b7SZebra North
283c7f6b7b7SZebra North        // IPs in order of most to least preferred.
284c7f6b7b7SZebra North        $ips = [];
285c7f6b7b7SZebra North
2862b760c9fSAlexander Lehmann        // Use a custom IP header (e.g. CDN) if it is set by the configuration.
2872b760c9fSAlexander Lehmann        if (!empty($conf['client_ip_header']) && $INPUT->server->str('HTTP_' . $conf['client_ip_header'])) {
2882b760c9fSAlexander Lehmann            $ips[] = $INPUT->server->str('HTTP_' . $conf['client_ip_header']);
289c7f6b7b7SZebra North        }
290c7f6b7b7SZebra North
291c7f6b7b7SZebra North        // Add the X-Forwarded-For addresses if all proxies are trusted.
292c7f6b7b7SZebra North        $ips = array_merge($ips, Ip::forwardedFor());
293c7f6b7b7SZebra North
294c7f6b7b7SZebra North        // Add the TCP/IP connection endpoint.
295c7f6b7b7SZebra North        $ips[] = $INPUT->server->str('REMOTE_ADDR');
296c7f6b7b7SZebra North
297c7f6b7b7SZebra North        // Remove invalid IPs.
298e449acd0SAndreas Gohr        $ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP));
299c7f6b7b7SZebra North
300c7f6b7b7SZebra North        // Remove duplicated IPs.
301c7f6b7b7SZebra North        $ips = array_values(array_unique($ips));
302c7f6b7b7SZebra North
303c7f6b7b7SZebra North        // Add a fallback if for some reason there were no valid IPs.
304c7f6b7b7SZebra North        if (!$ips) {
305c7f6b7b7SZebra North            $ips[] = '0.0.0.0';
306c7f6b7b7SZebra North        }
307c7f6b7b7SZebra North
308c7f6b7b7SZebra North        return $ips;
309c7f6b7b7SZebra North    }
31033cb4e01SAndreas Gohr
31133cb4e01SAndreas Gohr    /**
31233cb4e01SAndreas Gohr     * Get the host name of the server.
31333cb4e01SAndreas Gohr     *
31433cb4e01SAndreas Gohr     * The host name is sourced from, in order of preference:
31533cb4e01SAndreas Gohr     *
31633cb4e01SAndreas Gohr     *   - The X-Forwarded-Host header if it exists and the proxies are trusted.
31733cb4e01SAndreas Gohr     *   - The HTTP_HOST header.
31833cb4e01SAndreas Gohr     *   - The SERVER_NAME header.
31933cb4e01SAndreas Gohr     *   - The system's host name.
32033cb4e01SAndreas Gohr     *
32133cb4e01SAndreas Gohr     * @return string Returns the host name of the server.
32233cb4e01SAndreas Gohr     */
32333cb4e01SAndreas Gohr    public static function hostName(): string
32433cb4e01SAndreas Gohr    {
32533cb4e01SAndreas Gohr        /* @var Input $INPUT */
32633cb4e01SAndreas Gohr        global $INPUT;
32733cb4e01SAndreas Gohr
3287caad012SAndreas Gohr        $remoteAddr = $INPUT->server->str('REMOTE_ADDR');
3297caad012SAndreas Gohr        if ($INPUT->server->str('HTTP_X_FORWARDED_HOST') && self::proxyIsTrusted($remoteAddr)) {
33033cb4e01SAndreas Gohr            return $INPUT->server->str('HTTP_X_FORWARDED_HOST');
33133cb4e01SAndreas Gohr        } elseif ($INPUT->server->str('HTTP_HOST')) {
33233cb4e01SAndreas Gohr            return $INPUT->server->str('HTTP_HOST');
33333cb4e01SAndreas Gohr        } elseif ($INPUT->server->str('SERVER_NAME')) {
33433cb4e01SAndreas Gohr            return $INPUT->server->str('SERVER_NAME');
33533cb4e01SAndreas Gohr        } else {
33633cb4e01SAndreas Gohr            return php_uname('n');
33733cb4e01SAndreas Gohr        }
33833cb4e01SAndreas Gohr    }
33933cb4e01SAndreas Gohr
34033cb4e01SAndreas Gohr    /**
34133cb4e01SAndreas Gohr     * Is the connection using the HTTPS protocol?
34233cb4e01SAndreas Gohr     *
34333cb4e01SAndreas Gohr     * Will use the X-Forwarded-Proto header if it exists and the proxies are trusted, otherwise
34433cb4e01SAndreas Gohr     * the HTTPS environment is used.
34533cb4e01SAndreas Gohr     *
34633cb4e01SAndreas Gohr     * @return bool
34733cb4e01SAndreas Gohr     */
34833cb4e01SAndreas Gohr    public static function isSsl(): bool
34933cb4e01SAndreas Gohr    {
35033cb4e01SAndreas Gohr        /* @var Input $INPUT */
35133cb4e01SAndreas Gohr        global $INPUT;
35233cb4e01SAndreas Gohr
3537caad012SAndreas Gohr        $remoteAddr = $INPUT->server->str('REMOTE_ADDR');
3547caad012SAndreas Gohr        if ($INPUT->server->has('HTTP_X_FORWARDED_PROTO') && self::proxyIsTrusted($remoteAddr)) {
35533cb4e01SAndreas Gohr            return $INPUT->server->str('HTTP_X_FORWARDED_PROTO') === 'https';
35633cb4e01SAndreas Gohr        }
3577caad012SAndreas Gohr        return !preg_match('/^(|off|false|disabled)$/i', $INPUT->server->str('HTTPS', 'off'));
35833cb4e01SAndreas Gohr    }
359c7f6b7b7SZebra North}
360