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