1c7f6b7b7SZebra North<?php 2c7f6b7b7SZebra North 3c7f6b7b7SZebra North/** 433cb4e01SAndreas Gohr * DokuWiki IP address and reverse proxy 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; 1391da8d44SWillForanuse dokuwiki\Ip32; 14c7f6b7b7SZebra Northuse Exception; 15c7f6b7b7SZebra North 16c7f6b7b7SZebra Northclass Ip 17c7f6b7b7SZebra North{ 18c7f6b7b7SZebra North /** 19c7f6b7b7SZebra North * Determine whether an IP address is within a given CIDR range. 20c7f6b7b7SZebra North * The needle and haystack may be either IPv4 or IPv6. 21c7f6b7b7SZebra North * 22c7f6b7b7SZebra North * Example: 23c7f6b7b7SZebra North * 24c7f6b7b7SZebra North * ipInRange('192.168.11.123', '192.168.0.0/16') === true 25c7f6b7b7SZebra North * ipInRange('192.168.11.123', '::192.168.0.0/80') === true 26c7f6b7b7SZebra North * ipInRange('::192.168.11.123', '192.168.0.0/16') === true 27c7f6b7b7SZebra North * ipInRange('::192.168.11.123', '::192.168.0.0/80') === true 28c7f6b7b7SZebra North * 29e449acd0SAndreas Gohr * @param string $needle The IP to test, either IPv4 in dotted decimal 30c7f6b7b7SZebra North * notation or IPv6 in colon notation. 31c7f6b7b7SZebra North * @param string $haystack The CIDR range as an IP followed by a forward 32c7f6b7b7SZebra North * slash and the number of significant bits. 33c7f6b7b7SZebra North * 34c7f6b7b7SZebra North * @return bool Returns true if $needle is within the range specified 35c7f6b7b7SZebra North * by $haystack, false if it is outside the range. 36c7f6b7b7SZebra North * 37c7f6b7b7SZebra North * @throws Exception Thrown if $needle is not a valid IP address. 38c7f6b7b7SZebra North * @throws Exception Thrown if $haystack is not a valid IP range. 39c7f6b7b7SZebra North */ 40c7f6b7b7SZebra North public static function ipInRange(string $needle, string $haystack): bool 41c7f6b7b7SZebra North { 42c7f6b7b7SZebra North $range = explode('/', $haystack); 43c7f6b7b7SZebra North $networkIp = Ip::ipToNumber($range[0]); 44*40981bccSAndreas Gohr 45*40981bccSAndreas Gohr // The mask length must be a non-negative integer. 46*40981bccSAndreas Gohr if (!isset($range[1]) || !ctype_digit($range[1])) { 47*40981bccSAndreas Gohr throw new Exception('Invalid IP range mask: ' . $haystack); 48*40981bccSAndreas Gohr } 49*40981bccSAndreas Gohr $maskLength = (int) $range[1]; 50c7f6b7b7SZebra North 51c7f6b7b7SZebra North // For an IPv4 address the top 96 bits must be zero. 52c7f6b7b7SZebra North if ($networkIp['version'] === 4) { 53c7f6b7b7SZebra North $maskLength += 96; 54c7f6b7b7SZebra North } 55c7f6b7b7SZebra North 56c7f6b7b7SZebra North if ($maskLength > 128) { 57c7f6b7b7SZebra North throw new Exception('Invalid IP range mask: ' . $haystack); 58c7f6b7b7SZebra North } 59c7f6b7b7SZebra North 60c7f6b7b7SZebra North 61c7f6b7b7SZebra North $needle = Ip::ipToNumber($needle); 62c7f6b7b7SZebra North 6325a70af9SWillForan $maskLengthUpper = min($maskLength, 64); 6425a70af9SWillForan $maskLengthLower = max(0, $maskLength - 64); 6525a70af9SWillForan 6625a70af9SWillForan if (PHP_INT_SIZE == 4) { 67a060f5a0SWillForan $needle_up = Ip32::bitmask64On32($needle['upper'], $maskLengthUpper); 68a060f5a0SWillForan $net_up = Ip32::bitmask64On32($networkIp['upper'], $maskLengthUpper); 69a060f5a0SWillForan $needle_lo = Ip32::bitmask64On32($needle['lower'], $maskLengthLower); 70a060f5a0SWillForan $net_lo = Ip32::bitmask64On32($networkIp['lower'], $maskLengthLower); 7125a70af9SWillForan } else { 7225a70af9SWillForan $maskUpper = ~0 << intval(64 - $maskLengthUpper); 7325a70af9SWillForan $maskLower = ~0 << intval(64 - $maskLengthLower); 7425a70af9SWillForan 7525a70af9SWillForan $needle_up = $needle['upper'] & $maskUpper; 7625a70af9SWillForan $net_up = $networkIp['upper'] & $maskUpper; 7725a70af9SWillForan $needle_lo = $needle['lower'] & $maskLower; 7825a70af9SWillForan $net_lo = $networkIp['lower'] & $maskLower; 7925a70af9SWillForan } 8025a70af9SWillForan 8125a70af9SWillForan return $needle_up === $net_up && $needle_lo === $net_lo; 82c7f6b7b7SZebra North } 83c7f6b7b7SZebra North 84c7f6b7b7SZebra North /** 85c7f6b7b7SZebra North * Convert an IP address from a string to a number. 86c7f6b7b7SZebra North * 87c7f6b7b7SZebra North * This splits 128 bit IP addresses into the upper and lower 64 bits, and 88c7f6b7b7SZebra North * also returns whether the IP given was IPv4 or IPv6. 89c7f6b7b7SZebra North * 90c7f6b7b7SZebra North * The returned array contains: 91c7f6b7b7SZebra North * 92c7f6b7b7SZebra North * - version: Either '4' or '6'. 93c7f6b7b7SZebra North * - upper: The upper 64 bits of the IP. 94c7f6b7b7SZebra North * - lower: The lower 64 bits of the IP. 95c7f6b7b7SZebra North * 96c7f6b7b7SZebra North * For an IPv4 address, 'upper' will always be zero. 97c7f6b7b7SZebra North * 98e449acd0SAndreas Gohr * @param string $ip The IPv4 or IPv6 address. 99c7f6b7b7SZebra North * 100c7f6b7b7SZebra North * @return int[] Returns an array of 'version', 'upper', 'lower'. 101c7f6b7b7SZebra North * 102c7f6b7b7SZebra North * @throws Exception Thrown if the IP is not valid. 103c7f6b7b7SZebra North */ 104c7f6b7b7SZebra North public static function ipToNumber(string $ip): array 105c7f6b7b7SZebra North { 106c7f6b7b7SZebra North $binary = inet_pton($ip); 107c7f6b7b7SZebra North 108c7f6b7b7SZebra North if ($binary === false) { 109c7f6b7b7SZebra North throw new Exception('Invalid IP: ' . $ip); 110c7f6b7b7SZebra North } 111c7f6b7b7SZebra North 112c7f6b7b7SZebra North if (strlen($binary) === 4) { 113c7f6b7b7SZebra North // IPv4. 1142f70db90SWillForan $ipNum = unpack('Nip', $binary)['ip']; 1152f70db90SWillForan if (PHP_INT_SIZE == 4) { 1162f70db90SWillForan // integer overlfow on 32bit: negative even though 'N'=unsigned 1172f70db90SWillForan $ipNum = ($ipNum < 0) ? bcadd($ipNum, Ip32::$b32) : (string)$ipNum; 1182f70db90SWillForan } 119c7f6b7b7SZebra North return [ 120c7f6b7b7SZebra North 'version' => 4, 121c7f6b7b7SZebra North 'upper' => 0, 1222f70db90SWillForan 'lower' => $ipNum, 123c7f6b7b7SZebra North ]; 124c7f6b7b7SZebra North } else { 125da569c7fSWillForan // IPv6. strlen==16 1262f70db90SWillForan if (PHP_INT_SIZE == 4) { // 32-bit 127a060f5a0SWillForan $result = Ip32::ipv6UpperLowerOn32($binary); 1282f70db90SWillForan } else { // 64-bit arch 129c7f6b7b7SZebra North $result = unpack('Jupper/Jlower', $binary); 130c9e618caSWillForan } 131c7f6b7b7SZebra North $result['version'] = 6; 132c7f6b7b7SZebra North return $result; 133c7f6b7b7SZebra North } 134c7f6b7b7SZebra North } 135c7f6b7b7SZebra North 136c7f6b7b7SZebra North /** 137c7f6b7b7SZebra North * Determine if an IP address is equal to another IP or within an IP range. 138c7f6b7b7SZebra North * IPv4 and IPv6 are supported. 139c7f6b7b7SZebra North * 140c7f6b7b7SZebra North * @param string $ip The address to test. 141c7f6b7b7SZebra North * @param string $ipOrRange An IP address or CIDR range. 142c7f6b7b7SZebra North * 143c7f6b7b7SZebra North * @return bool Returns true if the IP matches, false if not. 144c7f6b7b7SZebra North */ 145c7f6b7b7SZebra North public static function ipMatches(string $ip, string $ipOrRange): bool 146c7f6b7b7SZebra North { 147c7f6b7b7SZebra North try { 148c7f6b7b7SZebra North // If it's not a range, compare the addresses directly. 149c7f6b7b7SZebra North // Addresses are converted to numbers because the same address may be 150c7f6b7b7SZebra North // represented by different strings, e.g. "::1" and "::0001". 151093fe67eSAndreas Gohr if (!str_contains($ipOrRange, '/')) { 152c7f6b7b7SZebra North return Ip::ipToNumber($ip) === Ip::ipToNumber($ipOrRange); 153c7f6b7b7SZebra North } 154c7f6b7b7SZebra North 155c7f6b7b7SZebra North return Ip::ipInRange($ip, $ipOrRange); 156*40981bccSAndreas Gohr } catch (\Throwable) { 157*40981bccSAndreas Gohr // The IP address or range was invalid. 158c7f6b7b7SZebra North return false; 159c7f6b7b7SZebra North } 160c7f6b7b7SZebra North } 161c7f6b7b7SZebra North 162c7f6b7b7SZebra North /** 163c7f6b7b7SZebra North * Given the IP address of a proxy server, determine whether it is 164c7f6b7b7SZebra North * a known and trusted server. 165c7f6b7b7SZebra North * 16619d5ba27SAndreas Gohr * This test is performed using the config value `trustedproxies`. 167c7f6b7b7SZebra North * 168c7f6b7b7SZebra North * @param string $ip The IP address of the proxy. 169c7f6b7b7SZebra North * 170c7f6b7b7SZebra North * @return bool Returns true if the IP is trusted as a proxy. 171c7f6b7b7SZebra North */ 172c7f6b7b7SZebra North public static function proxyIsTrusted(string $ip): bool 173c7f6b7b7SZebra North { 174c7f6b7b7SZebra North global $conf; 175c7f6b7b7SZebra North 176c7f6b7b7SZebra North // If the configuration is empty then no proxies are trusted. 17719d5ba27SAndreas Gohr if (empty($conf['trustedproxies'])) { 178c7f6b7b7SZebra North return false; 179c7f6b7b7SZebra North } 180c7f6b7b7SZebra North 18119d5ba27SAndreas Gohr foreach ((array)$conf['trustedproxies'] as $trusted) { 182c7f6b7b7SZebra North if (Ip::ipMatches($ip, $trusted)) { 183ced0b55fSAndreas Gohr return true; // The given IP matches one of the trusted proxies. 184c7f6b7b7SZebra North } 185c7f6b7b7SZebra North } 186c7f6b7b7SZebra North 187ced0b55fSAndreas Gohr return false; // none of the proxies matched 188c7f6b7b7SZebra North } 189c7f6b7b7SZebra North 190c7f6b7b7SZebra North /** 191c7f6b7b7SZebra North * Get the originating IP address and the address of every proxy that the 192c7f6b7b7SZebra North * request has passed through, according to the X-Forwarded-For header. 193c7f6b7b7SZebra North * 194c7f6b7b7SZebra North * To prevent spoofing of the client IP, every proxy listed in the 195c7f6b7b7SZebra North * X-Forwarded-For header must be trusted, as well as the TCP/IP endpoint 196c7f6b7b7SZebra North * from which the connection was received (i.e. the final proxy). 197c7f6b7b7SZebra North * 198c7f6b7b7SZebra North * If the header is not present or contains an untrusted proxy then 199c7f6b7b7SZebra North * an empty array is returned. 200c7f6b7b7SZebra North * 201c7f6b7b7SZebra North * The client IP is the first entry in the returned list, followed by the 202c7f6b7b7SZebra North * proxies. 203c7f6b7b7SZebra North * 204c7f6b7b7SZebra North * @return string[] Returns an array of IP addresses. 205c7f6b7b7SZebra North */ 206c7f6b7b7SZebra North public static function forwardedFor(): array 207c7f6b7b7SZebra North { 208c7f6b7b7SZebra North /* @var Input $INPUT */ 209c7f6b7b7SZebra North global $INPUT, $conf; 210c7f6b7b7SZebra North 211c7f6b7b7SZebra North $forwardedFor = $INPUT->server->str('HTTP_X_FORWARDED_FOR'); 212c7f6b7b7SZebra North 213ced0b55fSAndreas Gohr if (empty($conf['trustedproxies']) || !$forwardedFor) { 214c7f6b7b7SZebra North return []; 215c7f6b7b7SZebra North } 216c7f6b7b7SZebra North 217c7f6b7b7SZebra North // This is the address from which the header was received. 218c7f6b7b7SZebra North $remoteAddr = $INPUT->server->str('REMOTE_ADDR'); 219c7f6b7b7SZebra North 220c7f6b7b7SZebra North // Get the client address from the X-Forwarded-For header. 221c7f6b7b7SZebra North // X-Forwarded-For: <client> [, <proxy>]... 222c7f6b7b7SZebra North $forwardedFor = explode(',', str_replace(' ', '', $forwardedFor)); 223c7f6b7b7SZebra North 224c7f6b7b7SZebra North // The client address is the first item, remove it from the list. 225c7f6b7b7SZebra North $clientAddress = array_shift($forwardedFor); 226c7f6b7b7SZebra North 227c7f6b7b7SZebra North // The remaining items are the proxies through which the X-Forwarded-For 228c7f6b7b7SZebra North // header has passed. The final proxy is the connection's remote address. 229c7f6b7b7SZebra North $proxies = $forwardedFor; 230c7f6b7b7SZebra North $proxies[] = $remoteAddr; 231c7f6b7b7SZebra North 232c7f6b7b7SZebra North // Ensure that every proxy is trusted. 233c7f6b7b7SZebra North foreach ($proxies as $proxy) { 234c7f6b7b7SZebra North if (!Ip::proxyIsTrusted($proxy)) { 235c7f6b7b7SZebra North return []; 236c7f6b7b7SZebra North } 237c7f6b7b7SZebra North } 238c7f6b7b7SZebra North 239c7f6b7b7SZebra North // Add the client address before the list of proxies. 240c7f6b7b7SZebra North return array_merge([$clientAddress], $proxies); 241c7f6b7b7SZebra North } 242c7f6b7b7SZebra North 243c7f6b7b7SZebra North /** 244c7f6b7b7SZebra North * Return the IP of the client. 245c7f6b7b7SZebra North * 246c7f6b7b7SZebra North * The IP is sourced from, in order of preference: 247c7f6b7b7SZebra North * 24890c2f6e3SAndreas Gohr * - The custom IP header if $conf[client_ip_header] is set. 249c7f6b7b7SZebra North * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxy]. 250c7f6b7b7SZebra North * - The TCP/IP connection remote address. 251c7f6b7b7SZebra North * - 0.0.0.0 if all else fails. 252c7f6b7b7SZebra North * 25390c2f6e3SAndreas Gohr * The 'client_ip_header' config value should only be set if the header 254c7f6b7b7SZebra North * is being added by the web server, otherwise it may be spoofed by the client. 255c7f6b7b7SZebra North * 256c7f6b7b7SZebra North * The 'trustedproxy' setting must not allow any IP, otherwise the X-Forwarded-For 257c7f6b7b7SZebra North * may be spoofed by the client. 258c7f6b7b7SZebra North * 259c7f6b7b7SZebra North * @return string Returns an IPv4 or IPv6 address. 260c7f6b7b7SZebra North */ 261c7f6b7b7SZebra North public static function clientIp(): string 262c7f6b7b7SZebra North { 263c7f6b7b7SZebra North return Ip::clientIps()[0]; 264c7f6b7b7SZebra North } 265c7f6b7b7SZebra North 266c7f6b7b7SZebra North /** 267c7f6b7b7SZebra North * Return the IP of the client and the proxies through which the connection has passed. 268c7f6b7b7SZebra North * 269c7f6b7b7SZebra North * The IPs are sourced from, in order of preference: 270c7f6b7b7SZebra North * 27190c2f6e3SAndreas Gohr * - The custom IP header if $conf[client_ip_header] is set. 27219d5ba27SAndreas Gohr * - The X-Forwarded-For header if all the proxies are trusted by $conf[trustedproxies]. 273c7f6b7b7SZebra North * - The TCP/IP connection remote address. 274c7f6b7b7SZebra North * - 0.0.0.0 if all else fails. 275c7f6b7b7SZebra North * 276c7f6b7b7SZebra North * @return string[] Returns an array of IPv4 or IPv6 addresses. 277c7f6b7b7SZebra North */ 278c7f6b7b7SZebra North public static function clientIps(): array 279c7f6b7b7SZebra North { 280c7f6b7b7SZebra North /* @var Input $INPUT */ 281c7f6b7b7SZebra North global $INPUT, $conf; 282c7f6b7b7SZebra North 283c7f6b7b7SZebra North // IPs in order of most to least preferred. 284c7f6b7b7SZebra North $ips = []; 285c7f6b7b7SZebra North 2862b760c9fSAlexander Lehmann // Use a custom IP header (e.g. CDN) if it is set by the configuration. 2872b760c9fSAlexander Lehmann if (!empty($conf['client_ip_header']) && $INPUT->server->str('HTTP_' . $conf['client_ip_header'])) { 2882b760c9fSAlexander Lehmann $ips[] = $INPUT->server->str('HTTP_' . $conf['client_ip_header']); 289c7f6b7b7SZebra North } 290c7f6b7b7SZebra North 291c7f6b7b7SZebra North // Add the X-Forwarded-For addresses if all proxies are trusted. 292c7f6b7b7SZebra North $ips = array_merge($ips, Ip::forwardedFor()); 293c7f6b7b7SZebra North 294c7f6b7b7SZebra North // Add the TCP/IP connection endpoint. 295c7f6b7b7SZebra North $ips[] = $INPUT->server->str('REMOTE_ADDR'); 296c7f6b7b7SZebra North 297c7f6b7b7SZebra North // Remove invalid IPs. 298e449acd0SAndreas Gohr $ips = array_filter($ips, static fn($ip) => filter_var($ip, FILTER_VALIDATE_IP)); 299c7f6b7b7SZebra North 300c7f6b7b7SZebra North // Remove duplicated IPs. 301c7f6b7b7SZebra North $ips = array_values(array_unique($ips)); 302c7f6b7b7SZebra North 303c7f6b7b7SZebra North // Add a fallback if for some reason there were no valid IPs. 304c7f6b7b7SZebra North if (!$ips) { 305c7f6b7b7SZebra North $ips[] = '0.0.0.0'; 306c7f6b7b7SZebra North } 307c7f6b7b7SZebra North 308c7f6b7b7SZebra North return $ips; 309c7f6b7b7SZebra North } 31033cb4e01SAndreas Gohr 31133cb4e01SAndreas Gohr /** 31233cb4e01SAndreas Gohr * Get the host name of the server. 31333cb4e01SAndreas Gohr * 31433cb4e01SAndreas Gohr * The host name is sourced from, in order of preference: 31533cb4e01SAndreas Gohr * 31633cb4e01SAndreas Gohr * - The X-Forwarded-Host header if it exists and the proxies are trusted. 31733cb4e01SAndreas Gohr * - The HTTP_HOST header. 31833cb4e01SAndreas Gohr * - The SERVER_NAME header. 31933cb4e01SAndreas Gohr * - The system's host name. 32033cb4e01SAndreas Gohr * 32133cb4e01SAndreas Gohr * @return string Returns the host name of the server. 32233cb4e01SAndreas Gohr */ 32333cb4e01SAndreas Gohr public static function hostName(): string 32433cb4e01SAndreas Gohr { 32533cb4e01SAndreas Gohr /* @var Input $INPUT */ 32633cb4e01SAndreas Gohr global $INPUT; 32733cb4e01SAndreas Gohr 3287caad012SAndreas Gohr $remoteAddr = $INPUT->server->str('REMOTE_ADDR'); 3297caad012SAndreas Gohr if ($INPUT->server->str('HTTP_X_FORWARDED_HOST') && self::proxyIsTrusted($remoteAddr)) { 33033cb4e01SAndreas Gohr return $INPUT->server->str('HTTP_X_FORWARDED_HOST'); 33133cb4e01SAndreas Gohr } elseif ($INPUT->server->str('HTTP_HOST')) { 33233cb4e01SAndreas Gohr return $INPUT->server->str('HTTP_HOST'); 33333cb4e01SAndreas Gohr } elseif ($INPUT->server->str('SERVER_NAME')) { 33433cb4e01SAndreas Gohr return $INPUT->server->str('SERVER_NAME'); 33533cb4e01SAndreas Gohr } else { 33633cb4e01SAndreas Gohr return php_uname('n'); 33733cb4e01SAndreas Gohr } 33833cb4e01SAndreas Gohr } 33933cb4e01SAndreas Gohr 34033cb4e01SAndreas Gohr /** 34133cb4e01SAndreas Gohr * Is the connection using the HTTPS protocol? 34233cb4e01SAndreas Gohr * 34333cb4e01SAndreas Gohr * Will use the X-Forwarded-Proto header if it exists and the proxies are trusted, otherwise 34433cb4e01SAndreas Gohr * the HTTPS environment is used. 34533cb4e01SAndreas Gohr * 34633cb4e01SAndreas Gohr * @return bool 34733cb4e01SAndreas Gohr */ 34833cb4e01SAndreas Gohr public static function isSsl(): bool 34933cb4e01SAndreas Gohr { 35033cb4e01SAndreas Gohr /* @var Input $INPUT */ 35133cb4e01SAndreas Gohr global $INPUT; 35233cb4e01SAndreas Gohr 3537caad012SAndreas Gohr $remoteAddr = $INPUT->server->str('REMOTE_ADDR'); 3547caad012SAndreas Gohr if ($INPUT->server->has('HTTP_X_FORWARDED_PROTO') && self::proxyIsTrusted($remoteAddr)) { 35533cb4e01SAndreas Gohr return $INPUT->server->str('HTTP_X_FORWARDED_PROTO') === 'https'; 35633cb4e01SAndreas Gohr } 3577caad012SAndreas Gohr return !preg_match('/^(|off|false|disabled)$/i', $INPUT->server->str('HTTPS', 'off')); 35833cb4e01SAndreas Gohr } 359c7f6b7b7SZebra North} 360