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