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