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