4 * DokuWiki IP address functions.
6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author Zebra North <mrzebra@mrzebra.co.uk>
12 use dokuwiki\Input\Input
;
18 * Determine whether an IP address is within a given CIDR range.
19 * The needle and haystack may be either IPv4 or IPv6.
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
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.
33 * @return bool Returns true if $needle is within the range specified
34 * by $haystack, false if it is outside the range.
36 * @throws Exception Thrown if $needle is not a valid IP address.
37 * @throws Exception Thrown if $haystack is not a valid IP range.
39 public static function ipInRange(string $needle, string $haystack): bool
41 $range = explode('/', $haystack);
42 $networkIp = Ip
::ipToNumber($range[0]);
43 $maskLength = $range[1];
45 // For an IPv4 address the top 96 bits must be zero.
46 if ($networkIp['version'] === 4) {
50 if ($maskLength > 128) {
51 throw new Exception('Invalid IP range mask: ' . $haystack);
54 $maskLengthUpper = min($maskLength, 64);
55 $maskLengthLower = max(0, $maskLength - 64);
57 $maskUpper = ~
0 << intval(64 - $maskLengthUpper);
58 $maskLower = ~
0 << intval(64 - $maskLengthLower);
60 $needle = Ip
::ipToNumber($needle);
62 return ($needle['upper'] & $maskUpper) === ($networkIp['upper'] & $maskUpper) &&
63 ($needle['lower'] & $maskLower) === ($networkIp['lower'] & $maskLower);
67 * Convert an IP address from a string to a number.
69 * This splits 128 bit IP addresses into the upper and lower 64 bits, and
70 * also returns whether the IP given was IPv4 or IPv6.
72 * The returned array contains:
74 * - version: Either '4' or '6'.
75 * - upper: The upper 64 bits of the IP.
76 * - lower: The lower 64 bits of the IP.
78 * For an IPv4 address, 'upper' will always be zero.
80 * @param string $ip The IPv4 or IPv6 address.
82 * @return int[] Returns an array of 'version', 'upper', 'lower'.
84 * @throws Exception Thrown if the IP is not valid.
86 public static function ipToNumber(string $ip): array
88 $binary = inet_pton($ip);
90 if ($binary === false) {
91 throw new Exception('Invalid IP: ' . $ip);
94 if (strlen($binary) === 4) {
99 'lower' => unpack('Nip', $binary)['ip'],
103 $result = unpack('Jupper/Jlower', $binary);
104 $result['version'] = 6;
110 * Determine if an IP address is equal to another IP or within an IP range.
111 * IPv4 and IPv6 are supported.
113 * @param string $ip The address to test.
114 * @param string $ipOrRange An IP address or CIDR range.
116 * @return bool Returns true if the IP matches, false if not.
118 public static function ipMatches(string $ip, string $ipOrRange): bool
121 // If it's not a range, compare the addresses directly.
122 // Addresses are converted to numbers because the same address may be
123 // represented by different strings, e.g. "::1" and "::0001".
124 if (strpos($ipOrRange, '/') === false) {
125 return Ip
::ipToNumber($ip) === Ip
::ipToNumber($ipOrRange);
128 return Ip
::ipInRange($ip, $ipOrRange);
129 } catch (Exception
$ex) {
130 // The IP address was invalid.
136 * Given the IP address of a proxy server, determine whether it is
137 * a known and trusted server.
139 * This test is performed using the config value `trustedproxies`.
141 * @param string $ip The IP address of the proxy.
143 * @return bool Returns true if the IP is trusted as a proxy.
145 public static function proxyIsTrusted(string $ip): bool
149 // If the configuration is empty then no proxies are trusted.
150 if (empty($conf['trustedproxies'])) {
154 foreach ((array)$conf['trustedproxies'] as $trusted) {
155 if (Ip
::ipMatches($ip, $trusted)) {
156 return true; // The given IP matches one of the trusted proxies.
160 return false; // none of the proxies matched
164 * Get the originating IP address and the address of every proxy that the
165 * request has passed through, according to the X-Forwarded-For header.
167 * To prevent spoofing of the client IP, every proxy listed in the
168 * X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint
169 * from which the connection was received (i.e. the final proxy).
171 * If the header is not present or contains an untrusted proxy then
172 * an empty array is returned.
174 * The client IP is the first entry in the returned list, followed by the
177 * @return string[] Returns an array of IP addresses.
179 public static function forwardedFor(): array
181 /* @var Input $INPUT */
182 global $INPUT, $conf;
184 $forwardedFor = $INPUT->server
->str('HTTP_X_FORWARDED_FOR');
186 if (empty($conf['trustedproxies']) ||
!$forwardedFor) {
190 // This is the address from which the header was received.
191 $remoteAddr = $INPUT->server
->str('REMOTE_ADDR');
193 // Get the client address from the X-Forwarded-For header.
194 // X-Forwarded-For: <client> [, <proxy>]...
195 $forwardedFor = explode(',', str_replace(' ', '', $forwardedFor));
197 // The client address is the first item, remove it from the list.
198 $clientAddress = array_shift($forwardedFor);
200 // The remaining items are the proxies through which the X-Forwarded-For
201 // header has passed. The final proxy is the connection's remote address.
202 $proxies = $forwardedFor;
203 $proxies[] = $remoteAddr;
205 // Ensure that every proxy is trusted.
206 foreach ($proxies as $proxy) {
207 if (!Ip
::proxyIsTrusted($proxy)) {
212 // Add the client address before the list of proxies.
213 return array_merge([$clientAddress], $proxies);
217 * Return the IP of the client.
219 * The IP is sourced from, in order of preference:
221 * - The X-Real-IP header if $conf[realip] is true.
222 * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy].
223 * - The TCP/IP connection remote address.
224 * - 0.0.0.0 if all else fails.
226 * The 'realip' config value should only be set to true if the X-Real-IP header
227 * is being added by the web server, otherwise it may be spoofed by the client.
229 * The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For
230 * may be spoofed by the client.
232 * @return string Returns an IPv4 or IPv6 address.
234 public static function clientIp(): string
236 return Ip
::clientIps()[0];
240 * Return the IP of the client and the proxies through which the connection has passed.
242 * The IPs are sourced from, in order of preference:
244 * - The X-Real-IP header if $conf[realip] is true.
245 * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies].
246 * - The TCP/IP connection remote address.
247 * - 0.0.0.0 if all else fails.
249 * @return string[] Returns an array of IPv4 or IPv6 addresses.
251 public static function clientIps(): array
253 /* @var Input $INPUT */
254 global $INPUT, $conf;
256 // IPs in order of most to least preferred.
259 // Use the X-Real-IP header if it is enabled by the configuration.
260 if (!empty($conf['realip']) && $INPUT->server
->str('HTTP_X_REAL_IP')) {
261 $ips[] = $INPUT->server
->str('HTTP_X_REAL_IP');
264 // Add the X-Forwarded-For addresses if all proxies are trusted.
265 $ips = array_merge($ips, Ip
::forwardedFor());
267 // Add the TCP/IP connection endpoint.
268 $ips[] = $INPUT->server
->str('REMOTE_ADDR');
270 // Remove invalid IPs.
271 $ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP
));
273 // Remove duplicated IPs.
274 $ips = array_values(array_unique($ips));
276 // Add a fallback if for some reason there were no valid IPs.