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