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