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