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 $maskLengthUpper = min($maskLength, 64); 55 $maskLengthLower = max(0, $maskLength - 64); 56 57 $maskUpper = ~0 << intval(64 - $maskLengthUpper); 58 $maskLower = ~0 << intval(64 - $maskLengthLower); 59 60 $needle = Ip::ipToNumber($needle); 61 62 return ($needle['upper'] & $maskUpper) === ($networkIp['upper'] & $maskUpper) && 63 ($needle['lower'] & $maskLower) === ($networkIp['lower'] & $maskLower); 64 } 65 66 /** 67 * Convert an IP address from a string to a number. 68 * 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. 71 * 72 * The returned array contains: 73 * 74 * - version: Either '4' or '6'. 75 * - upper: The upper 64 bits of the IP. 76 * - lower: The lower 64 bits of the IP. 77 * 78 * For an IPv4 address, 'upper' will always be zero. 79 * 80 * @param string $ip The IPv4 or IPv6 address. 81 * 82 * @return int[] Returns an array of 'version', 'upper', 'lower'. 83 * 84 * @throws Exception Thrown if the IP is not valid. 85 */ 86 public static function ipToNumber(string $ip): array 87 { 88 $binary = inet_pton($ip); 89 90 if ($binary === false) { 91 throw new Exception('Invalid IP: ' . $ip); 92 } 93 94 if (strlen($binary) === 4) { 95 // IPv4. 96 return [ 97 'version' => 4, 98 'upper' => 0, 99 'lower' => unpack('Nip', $binary)['ip'], 100 ]; 101 } else { 102 // IPv6. strlen==16 103 if(PHP_INT_SIZE == 8) { // 64-bit arch 104 $result = unpack('Jupper/Jlower', $binary); 105 } else { // 32-bit 106 $result = Ip::ipv6_upper_lower_32($binary); 107 } 108 $result['version'] = 6; 109 return $result; 110 } 111 } 112 113 /** 114 * conversion of inet_pton ipv6 into 64-bit upper and lower 115 * bcmath version for 32-bit architecture 116 * w/o no unpack('J') - unsigned long long (always 64 bit, big endian byte order) 117 * 118 * results match unpack('Jupper/Jlower', $binary) 119 * 120 * @param string $binary inet_pton's ipv6 16 element binary 121 * 122 * @return int[] upper 64 and lower 64 for ipToNumber 123 */ 124 public static function ipv6_upper_lower_32(string $binary) { 125 // unpack into four 32-bit unsigned ints to recombine as 2 64-bit 126 $b32 = 4294967296; // bcpow(2, 32) 127 $parts = unpack('N4', $binary); 128 $upper = bcadd(bcmul($parts[1], $b32), 129 $parts[2]); 130 $lower = bcadd(bcmul($parts[3], $b32), 131 $parts[4]); 132 return ['upper' => $upper, 'lower' => $lower]; 133 } 134 135 /** 136 * Determine if an IP address is equal to another IP or within an IP range. 137 * IPv4 and IPv6 are supported. 138 * 139 * @param string $ip The address to test. 140 * @param string $ipOrRange An IP address or CIDR range. 141 * 142 * @return bool Returns true if the IP matches, false if not. 143 */ 144 public static function ipMatches(string $ip, string $ipOrRange): bool 145 { 146 try { 147 // If it's not a range, compare the addresses directly. 148 // Addresses are converted to numbers because the same address may be 149 // represented by different strings, e.g. "::1" and "::0001". 150 if (strpos($ipOrRange, '/') === false) { 151 return Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange); 152 } 153 154 return Ip::ipInRange($ip, $ipOrRange); 155 } catch (Exception $ex) { 156 // The IP address was invalid. 157 return false; 158 } 159 } 160 161 /** 162 * Given the IP address of a proxy server, determine whether it is 163 * a known and trusted server. 164 * 165 * This test is performed using the config value `trustedproxies`. 166 * 167 * @param string $ip The IP address of the proxy. 168 * 169 * @return bool Returns true if the IP is trusted as a proxy. 170 */ 171 public static function proxyIsTrusted(string $ip): bool 172 { 173 global $conf; 174 175 // If the configuration is empty then no proxies are trusted. 176 if (empty($conf['trustedproxies'])) { 177 return false; 178 } 179 180 foreach ((array)$conf['trustedproxies'] as $trusted) { 181 if (Ip::ipMatches($ip, $trusted)) { 182 return true; // The given IP matches one of the trusted proxies. 183 } 184 } 185 186 return false; // none of the proxies matched 187 } 188 189 /** 190 * Get the originating IP address and the address of every proxy that the 191 * request has passed through, according to the X-Forwarded-For header. 192 * 193 * To prevent spoofing of the client IP, every proxy listed in the 194 * X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint 195 * from which the connection was received (i.e. the final proxy). 196 * 197 * If the header is not present or contains an untrusted proxy then 198 * an empty array is returned. 199 * 200 * The client IP is the first entry in the returned list, followed by the 201 * proxies. 202 * 203 * @return string[] Returns an array of IP addresses. 204 */ 205 public static function forwardedFor(): array 206 { 207 /* @var Input $INPUT */ 208 global $INPUT, $conf; 209 210 $forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR'); 211 212 if (empty($conf['trustedproxies']) || !$forwardedFor) { 213 return []; 214 } 215 216 // This is the address from which the header was received. 217 $remoteAddr = $INPUT->server->str('REMOTE_ADDR'); 218 219 // Get the client address from the X-Forwarded-For header. 220 // X-Forwarded-For: <client> [, <proxy>]... 221 $forwardedFor = explode(',', str_replace(' ', '', $forwardedFor)); 222 223 // The client address is the first item, remove it from the list. 224 $clientAddress = array_shift($forwardedFor); 225 226 // The remaining items are the proxies through which the X-Forwarded-For 227 // header has passed. The final proxy is the connection's remote address. 228 $proxies = $forwardedFor; 229 $proxies[] = $remoteAddr; 230 231 // Ensure that every proxy is trusted. 232 foreach ($proxies as $proxy) { 233 if (!Ip::proxyIsTrusted($proxy)) { 234 return []; 235 } 236 } 237 238 // Add the client address before the list of proxies. 239 return array_merge([$clientAddress], $proxies); 240 } 241 242 /** 243 * Return the IP of the client. 244 * 245 * The IP is sourced from, in order of preference: 246 * 247 * - The X-Real-IP header if $conf[realip] is true. 248 * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy]. 249 * - The TCP/IP connection remote address. 250 * - 0.0.0.0 if all else fails. 251 * 252 * The 'realip' config value should only be set to true if the X-Real-IP header 253 * is being added by the web server, otherwise it may be spoofed by the client. 254 * 255 * The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For 256 * may be spoofed by the client. 257 * 258 * @return string Returns an IPv4 or IPv6 address. 259 */ 260 public static function clientIp(): string 261 { 262 return Ip::clientIps()[0]; 263 } 264 265 /** 266 * Return the IP of the client and the proxies through which the connection has passed. 267 * 268 * The IPs are sourced from, in order of preference: 269 * 270 * - The X-Real-IP header if $conf[realip] is true. 271 * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies]. 272 * - The TCP/IP connection remote address. 273 * - 0.0.0.0 if all else fails. 274 * 275 * @return string[] Returns an array of IPv4 or IPv6 addresses. 276 */ 277 public static function clientIps(): array 278 { 279 /* @var Input $INPUT */ 280 global $INPUT, $conf; 281 282 // IPs in order of most to least preferred. 283 $ips = []; 284 285 // Use the X-Real-IP header if it is enabled by the configuration. 286 if (!empty($conf['realip']) && $INPUT->server->str('HTTP_X_REAL_IP')) { 287 $ips[] = $INPUT->server->str('HTTP_X_REAL_IP'); 288 } 289 290 // Add the X-Forwarded-For addresses if all proxies are trusted. 291 $ips = array_merge($ips, Ip::forwardedFor()); 292 293 // Add the TCP/IP connection endpoint. 294 $ips[] = $INPUT->server->str('REMOTE_ADDR'); 295 296 // Remove invalid IPs. 297 $ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP)); 298 299 // Remove duplicated IPs. 300 $ips = array_values(array_unique($ips)); 301 302 // Add a fallback if for some reason there were no valid IPs. 303 if (!$ips) { 304 $ips[] = '0.0.0.0'; 305 } 306 307 return $ips; 308 } 309} 310