diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php new file mode 100644 index 00000000..ba4b025e --- /dev/null +++ b/Tests/IpHelperTest.php @@ -0,0 +1,265 @@ +backupServer = $_SERVER; + $this->backupEnv = $_ENV; + + unset($_SERVER['HTTP_CLIENT_IP']); + unset($_SERVER['HTTP_X_FORWARDED_FOR']); + unset($_SERVER['HTTP_X_FORWARDED']); + unset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']); + unset($_SERVER['HTTP_FORWARDED_FOR']); + unset($_SERVER['HTTP_FORWARDED']); + unset($_SERVER['REMOTE_ADDR']); + + IpHelper::setIP(null); + } + + /** + * Restore environment + */ + protected function tearDown() + { + $_SERVER = $this->backupServer; + $_ENV = $this->backupEnv; + } + + /** + * Sample client IPs + * + * @return array + */ + public function sampleClientIPs() + { + $indexes = array( + 'HTTP_X_FORWARDED_FOR', + 'HTTP_CLIENT_IP', + #'HTTP_X_FORWARDED', + #'HTTP_X_CLUSTER_CLIENT_IP', + #'HTTP_FORWARDED_FOR', + #'HTTP_FORWARDED', + 'REMOTE_ADDR', + ); + + // ip => normalised + $ips = array( + '127.0.0.1' => '127.0.0.1', + '192.168.178.32' => '192.168.178.32', + '10.194.95.79' => '10.194.95.79', + '75.184.124.93, 10.194.95.79' => '10.194.95.79', + '10.194.95.79, 75.184.124.93' => '75.184.124.93', + '0.0.0.0' => '0.0.0.0', + 'ff05::1' => 'ff05::1', + 'fake' => '', + ); + + $cases = array(); + + foreach ($indexes as $index) + { + foreach ($ips as $ip => $normalised) + { + $cases[] = array( + $index, + $ip, + $normalised + ); + } + } + + return $cases; + } + + /** + * @testdox IP address is retrieved from $_SERVER global + * + * @param string $index The index for the $_SERVER global + * @param string $ip The IP address in the global + * @param string $normalised The IP address to be returned + * + * @dataProvider sampleClientIPs + */ + public function testGetIpFromServerWithOverride($index, $ip, $normalised) + { + $_SERVER[$index] = $ip; + + IpHelper::setIP(null); + IpHelper::setAllowIpOverrides(true); + + $this->assertEquals($normalised, IpHelper::getIP()); + } + + /** + * @testdox IP address is retrieved from $_SERVER['REMOTE_ADDR'] if override is prohibited + * + * @param string $index The index for the $_SERVER global + * @param string $ip The IP address in the global + * @param string $normalised The IP address to be returned + * + * @dataProvider sampleClientIPs + */ + public function testGetIpFromServerWithoutOverride($index, $ip, $normalised) + { + $_SERVER[$index] = $ip; + $_SERVER['REMOTE_ADDR'] = '80.80.80.80'; + + IpHelper::setAllowIpOverrides(false); + + $this->assertEquals('80.80.80.80', IpHelper::getIP()); + } + + /** + * Sample IPs wit format information + * + * @return \string[][] + */ + public function sampleIPsWithFormat() + { + // ip => format + return array( + array('127.0.0.1', 'IPv4'), + array('::1', 'IPv6'), + array('::127.0.0.1', 'IPv6'), + array('fake:ip', 'invalid'), + ); + } + + /** + * @param string $ip The IP to check + * @param string $format The true format + * + * @dataProvider sampleIPsWithFormat + */ + public function testIsIp6($ip, $format) + { + $actual = IpHelper::isIPv6($ip); + $expected = $format === 'IPv6'; + + $this->assertEquals($expected, $actual); + } + + /** + * Sample IPs with IP Table information + * + * @return array[] + */ + public function sampleIPsWithTable() + { + // IP, IP Table, isInTable + return array( + 'IPv4 address - IPv4 address' => array(self::IPv4_ADDRESS, self::IPv4_ADDRESS, true), + + 'IPv4 address - IPv4 subnet' => array(self::IPv4_ADDRESS, self::IPv4_SUBNET, true), + 'IPv4 address - IPv4 network range' => array(self::IPv4_ADDRESS, self::IPv4_NETWORK_RANGE, true), + 'IPv4 address - IPv4 swapped range' => array(self::IPv4_ADDRESS, self::IPv4_SWAPPED_RANGE, true), + 'IPv4 address - IPv4 address/netmask' => array(self::IPv4_ADDRESS, self::IPv4_ADDRESS . '/' . self::IPv4_NETMASK, true), + 'IPv4 localhost - IPv4 subnets (list)' => array(self::IPv4_LOCALHOST, self::IPv4_SUBNET . ', ' . self::IPv4_LOCALHOST . '/8', true), + 'IPv4 localhost - IPv4 subnets (array)' => array(self::IPv4_LOCALHOST, array(self::IPv4_SUBNET, self::IPv4_LOCALHOST . '/8'), true), + + 'IPv4 address - 1 byte' => array(self::IPv4_LOCALHOST, '127.', true), + 'IPv4 address - 2 bytes' => array(self::IPv4_LOCALHOST, '127.0.', true), + 'IPv4 address - 3 bytes' => array(self::IPv4_LOCALHOST, '127.0.0.', true), + + 'IPv4 address - IPv6 expanded address' => array(self::IPv4_ADDRESS, self::IPv6_EXPANDED_ADDRESS, false), + 'IPv4 address - IPv6 subnet' => array(self::IPv4_ADDRESS, self::IPv6_SUBNET, false), + 'IPv4 address - IPv6 network range' => array(self::IPv4_ADDRESS, self::IPv6_NETWORK_RANGE, false), + + 'IPv4 any address - IPv4 subnet' => array(self::IPv4_ANY_ADDRESS, self::IPv4_SUBNET, false), + 'IPv4 localhost - IPv4 subnet' => array(self::IPv4_LOCALHOST, self::IPv4_SUBNET, false), + + 'empty - IPv4 subnet' => array(null, self::IPv4_SUBNET, false), + 'fake.ip - IPv4 subnet' => array('fake.ip', self::IPv4_SUBNET, false), + 'IPv4 address - empty range' => array(self::IPv4_ADDRESS, null, false), + 'IPv4 address - invalid.ip/range' => array(self::IPv4_ADDRESS, 'invalid.ip/range', false), + 'IPv4 address - partial invalid range' => array(self::IPv4_ADDRESS, self::IPv4_ADDRESS . '-invalid', false), + + 'IPv6 expanded address - IPv6 expanded address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_EXPANDED_ADDRESS, true), + 'IPv6 expanded address - IPv6 compressed address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS, true), + 'IPv6 compressed address - IPv6 expanded address' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_EXPANDED_ADDRESS, true), + 'IPv6 compressed address - IPv6 compressed address' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS, true), + + 'IPv6 expanded address - IPv6 subnet' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_SUBNET, true), + 'IPv6 expanded address - IPv6 network range' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_NETWORK_RANGE, true), + 'IPv6 expanded address - IPv6 swapped range' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_SWAPPED_RANGE, true), + 'IPv6 expanded address - IPv6 address/netmask' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv6_EXPANDED_ADDRESS . '/' . self::IPv6_NETMASK, true), + 'IPv6 compressed address - IPv6 subnet' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_SUBNET, true), + 'IPv6 compressed address - IPv6 network range' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_NETWORK_RANGE, true), + 'IPv6 compressed address - IPv6 swapped range' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_SWAPPED_RANGE, true), + 'IPv6 compressed address - IPv6 address/netmask' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_EXPANDED_ADDRESS . '/' . self::IPv6_NETMASK, true), + 'IPv6 localhost - IPv6 subnets (list)' => array(self::IPv6_LOCALHOST, self::IPv6_SUBNET . ', ' . self::IPv6_LOCALHOST . '/128', true), + 'IPv6 localhost - IPv6 subnets (array)' => array(self::IPv6_LOCALHOST, array(self::IPv6_SUBNET, self::IPv6_LOCALHOST . '/128'), true), + + 'IPv6 address - IPv4 address' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv4_ADDRESS, false), + 'IPv6 address - IPv4 subnet' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv4_SUBNET, false), + 'IPv6 address - IPv4 network range' => array(self::IPv6_EXPANDED_ADDRESS, self::IPv4_NETWORK_RANGE, false), + + 'IPv6 any address - IPv6 subnet' => array(self::IPv6_ANY_ADDRESS, self::IPv6_SUBNET, false), + 'IPv6 localhost - IPv6 subnet' => array(self::IPv6_LOCALHOST, self::IPv6_SUBNET, false), + + 'empty - IPv6 subnet' => array(null, self::IPv6_SUBNET, false), + 'fake:ip - IPv6 subnet' => array('fake:ip', self::IPv6_SUBNET, false), + 'IPv6 address - empty range' => array(self::IPv6_COMPRESSED_ADDRESS, null, false), + 'IPv6 address - invalid:ip/range' => array(self::IPv6_COMPRESSED_ADDRESS, 'invalid:ip/range', false), + 'IPv6 address - partial invalid range' => array(self::IPv6_COMPRESSED_ADDRESS, self::IPv6_COMPRESSED_ADDRESS . '-invalid', false), + ); + } + + /** + * @param string $ip + * @param string $ipTable + * @param boolean $expected + * + * @dataProvider sampleIPsWithTable + */ + public function testIpInList($ip, $ipTable, $expected) + { + $this->assertEquals($expected, IpHelper::isInRanges($ip, $ipTable)); + } +} diff --git a/src/IpHelper.php b/src/IpHelper.php index 1396354a..34d4657d 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -11,504 +11,464 @@ namespace Joomla\Utilities; /** - * IpHelper is a utility class for processing IP addresses + * Utility class for processing IP addresses * * @since 1.6.0 */ -final class IpHelper +abstract class IpHelper { - /** - * The IP address of the current visitor - * - * @var string - * @since 1.6.0 - */ - private static $ip = null; - - /** - * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? - * - * @var boolean - * @since 1.6.0 - * @note The default value is false in version 2.0+ - */ - private static $allowIpOverrides = false; - - /** - * Private constructor to prevent instantiation of this class - * - * @since 1.6.0 - */ - private function __construct() - { - } - - /** - * Get the current visitor's IP address - * - * @return string - * - * @since 1.6.0 - */ - public static function getIp() - { - if (self::$ip === null) { - $ip = static::detectAndCleanIP(); - - if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) { - $myIP = @inet_pton($ip); - - if ($myIP !== false) { - $ip = inet_ntop($myIP); - } - } - - static::setIp($ip); - } - - return self::$ip; - } - - /** - * Set the IP address of the current visitor - * - * @param string $ip The visitor's IP address - * - * @return void - * - * @since 1.6.0 - */ - public static function setIp($ip) - { - self::$ip = $ip; - } - - /** - * Is it an IPv6 IP address? - * - * @param string $ip An IPv4 or IPv6 address - * - * @return boolean - * - * @since 1.6.0 - */ - public static function isIPv6($ip) - { - return strpos($ip, ':') !== false; - } - - /** - * Checks if an IP is contained in a list of IPs or IP expressions - * - * @param string $ip The IPv4/IPv6 address to check - * @param array|string $ipTable An IP expression (or a comma-separated or array list of IP expressions) to check against - * - * @return boolean - * - * @since 1.6.0 - */ - public static function IPinList($ip, $ipTable = '') - { - // No point proceeding with an empty IP list - if (empty($ipTable)) { - return false; - } - - // If the IP list is not an array, convert it to an array - if (!\is_array($ipTable)) { - if (strpos($ipTable, ',') !== false) { - $ipTable = explode(',', $ipTable); - $ipTable = array_map('trim', $ipTable); - } else { - $ipTable = trim($ipTable); - $ipTable = [$ipTable]; - } - } - - // If no IP address is found, return false - if ($ip === '0.0.0.0') { - return false; - } - - // If no IP is given, return false - if (empty($ip)) { - return false; - } - - // Sanity check - if (!\function_exists('inet_pton')) { - return false; - } - - // Get the IP's in_adds representation - $myIP = @inet_pton($ip); - - // If the IP is in an unrecognisable format, quite - if ($myIP === false) { - return false; - } - - $ipv6 = static::isIPv6($ip); - - foreach ($ipTable as $ipExpression) { - $ipExpression = trim($ipExpression); - - // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 - if (strstr($ipExpression, '-')) { - list($from, $to) = explode('-', $ipExpression, 2); - - if ($ipv6 && (!static::isIPv6($from) || !static::isIPv6($to))) { - // Do not apply IPv4 filtering on an IPv6 address - continue; - } - - if (!$ipv6 && (static::isIPv6($from) || static::isIPv6($to))) { - // Do not apply IPv6 filtering on an IPv4 address - continue; - } - - $from = @inet_pton(trim($from)); - $to = @inet_pton(trim($to)); - - // Sanity check - if (($from === false) || ($to === false)) { - continue; - } - - // Swap from/to if they're in the wrong order - if ($from > $to) { - list($from, $to) = [$to, $from]; - } - - if (($myIP >= $from) && ($myIP <= $to)) { - return true; - } - } elseif (strstr($ipExpression, '/')) { - // Netmask or CIDR provided - $binaryip = static::inetToBits($myIP); - - list($net, $maskbits) = explode('/', $ipExpression, 2); - - if ($ipv6 && !static::isIPv6($net)) { - // Do not apply IPv4 filtering on an IPv6 address - continue; - } - - if (!$ipv6 && static::isIPv6($net)) { - // Do not apply IPv6 filtering on an IPv4 address - continue; - } - - if ($ipv6 && strstr($maskbits, ':')) { - // Perform an IPv6 CIDR check - if (static::checkIPv6CIDR($myIP, $ipExpression)) { - return true; - } - - // If we didn't match it proceed to the next expression - continue; - } - - if (!$ipv6 && strstr($maskbits, '.')) { - // Convert IPv4 netmask to CIDR - $long = ip2long($maskbits); - $base = ip2long('255.255.255.255'); - $maskbits = 32 - log(($long ^ $base) + 1, 2); - } - - // Convert network IP to in_addr representation - $net = @inet_pton($net); - - // Sanity check - if ($net === false) { - continue; - } - - // Get the network's binary representation - $expectedNumberOfBits = $ipv6 ? 128 : 24; - $binarynet = str_pad(static::inetToBits($net), $expectedNumberOfBits, '0', STR_PAD_RIGHT); - - // Check the corresponding bits of the IP and the network - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); - - if ($ipNetBits === $netBits) { - return true; - } - } else { - // IPv6: Only single IPs are supported - if ($ipv6) { - $ipExpression = trim($ipExpression); - - if (!static::isIPv6($ipExpression)) { - continue; - } - - $ipCheck = @inet_pton($ipExpression); - - if ($ipCheck === false) { - continue; - } - - if ($ipCheck == $myIP) { - return true; - } - } else { - // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] - $dots = 0; - - if (substr($ipExpression, -1) == '.') { - // Partial IP address. Convert to CIDR and re-match - foreach (count_chars($ipExpression, 1) as $i => $val) { - if ($i == 46) { - $dots = $val; - } - } - - switch ($dots) { - case 1: - $netmask = '255.0.0.0'; - $ipExpression .= '0.0.0'; - - break; - - case 2: - $netmask = '255.255.0.0'; - $ipExpression .= '0.0'; - - break; - - case 3: - $netmask = '255.255.255.0'; - $ipExpression .= '0'; - - break; - - default: - $dots = 0; - } - - if ($dots) { - $binaryip = static::inetToBits($myIP); - - // Convert netmask to CIDR - $long = ip2long($netmask); - $base = ip2long('255.255.255.255'); - $maskbits = 32 - log(($long ^ $base) + 1, 2); - - $net = @inet_pton($ipExpression); - - // Sanity check - if ($net === false) { - continue; - } - - // Get the network's binary representation - $expectedNumberOfBits = $ipv6 ? 128 : 24; - $binarynet = str_pad(static::inetToBits($net), $expectedNumberOfBits, '0', STR_PAD_RIGHT); - - // Check the corresponding bits of the IP and the network - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); - - if ($ipNetBits === $netBits) { - return true; - } - } - } - - if (!$dots) { - $ip = @inet_pton(trim($ipExpression)); - - if ($ip == $myIP) { - return true; - } - } - } - } - } - - return false; - } - - /** - * Works around the REMOTE_ADDR not containing the user's IP - * - * @return void - * - * @since 1.6.0 - */ - public static function workaroundIPIssues() - { - $ip = static::getIp(); - - if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === $ip) { - return; - } - - if (isset($_SERVER['REMOTE_ADDR'])) { - $_SERVER['JOOMLA_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; - } elseif (\function_exists('getenv')) { - if (getenv('REMOTE_ADDR')) { - $_SERVER['JOOMLA_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); - } - } - - $_SERVER['REMOTE_ADDR'] = $ip; - } - - /** - * Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header? - * - * @param boolean $newState True to allow the override - * - * @return void - * - * @since 1.6.0 - */ - public static function setAllowIpOverrides($newState) - { - self::$allowIpOverrides = $newState ? true : false; - } - - /** - * Gets the visitor's IP address. - * - * Automatically handles reverse proxies reporting the IPs of intermediate devices, like load balancers. Examples: - * - * - https://www.akeebabackup.com/support/admin-tools/13743-double-ip-adresses-in-security-exception-log-warnings.html - * - https://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips - * - * The solution used is assuming that the last IP address is the external one. - * - * @return string - * - * @since 1.6.0 - */ - protected static function detectAndCleanIP() - { - $ip = static::detectIP(); - - if (strstr($ip, ',') !== false || strstr($ip, ' ') !== false) { - $ip = str_replace(' ', ',', $ip); - $ip = str_replace(',,', ',', $ip); - $ips = explode(',', $ip); - $ip = ''; - - while (empty($ip) && !empty($ips)) { - $ip = array_shift($ips); - $ip = trim($ip); - } - } else { - $ip = trim($ip); - } - - return $ip; - } - - /** - * Gets the visitor's IP address - * - * @return string - * - * @since 1.6.0 - */ - protected static function detectIP() - { - // Normally the $_SERVER superglobal is set - if (isset($_SERVER)) { - // Do we have an x-forwarded-for HTTP header (e.g. NginX)? - if (self::$allowIpOverrides && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { - return $_SERVER['HTTP_X_FORWARDED_FOR']; - } - - // Do we have a client-ip header (e.g. non-transparent proxy)? - if (self::$allowIpOverrides && isset($_SERVER['HTTP_CLIENT_IP'])) { - return $_SERVER['HTTP_CLIENT_IP']; - } - - // Normal, non-proxied server or server behind a transparent proxy - if (isset($_SERVER['REMOTE_ADDR'])) { - return $_SERVER['REMOTE_ADDR']; - } - } - - /* - * This part is executed on PHP running as CGI, or on SAPIs which do not set the $_SERVER superglobal - * If getenv() is disabled, you're screwed - */ - if (!\function_exists('getenv')) { - return ''; - } - - // Do we have an x-forwarded-for HTTP header? - if (self::$allowIpOverrides && getenv('HTTP_X_FORWARDED_FOR')) { - return getenv('HTTP_X_FORWARDED_FOR'); - } - - // Do we have a client-ip header? - if (self::$allowIpOverrides && getenv('HTTP_CLIENT_IP')) { - return getenv('HTTP_CLIENT_IP'); - } - - // Normal, non-proxied server or server behind a transparent proxy - if (getenv('REMOTE_ADDR')) { - return getenv('REMOTE_ADDR'); - } - - // Catch-all case for broken servers, apparently - return ''; - } - - /** - * Converts inet_pton output to bits string - * - * @param string $inet The in_addr representation of an IPv4 or IPv6 address - * - * @return string - * - * @since 1.6.0 - */ - protected static function inetToBits($inet) - { - if (\strlen($inet) == 4) { - $unpacked = unpack('A4', $inet); - } else { - $unpacked = unpack('A16', $inet); - } - - $unpacked = str_split($unpacked[1]); - $binaryip = ''; - - foreach ($unpacked as $char) { - $binaryip .= str_pad(decbin(\ord($char)), 8, '0', STR_PAD_LEFT); - } - - return $binaryip; - } - - /** - * Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet - * - * @param string $ip The IPv6 address to check, e.g. 21DA:00D3:0000:2F3B:02AC:00FF:FE28:9C5A - * @param string $cidrnet The IPv6 CIDR block, e.g. 21DA:00D3:0000:2F3B::/64 - * - * @return boolean - * - * @since 1.6.0 - */ - protected static function checkIPv6CIDR($ip, $cidrnet) - { - $ip = inet_pton($ip); - $binaryip = static::inetToBits($ip); - - list($net, $maskbits) = explode('/', $cidrnet); - $net = inet_pton($net); - $binarynet = static::inetToBits($net); - - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); - - return $ipNetBits === $netBits; - } + /** + * The IP address of the current visitor + * + * @var string + * @since 1.6.0 + * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. + */ + private static $ip; + + /** + * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? + * + * @var boolean + * @since 1.6.0 + * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. + */ + private static $allowIpOverrides = true; + + /** + * Get the current visitor's IP address + * + * @param boolean $allowOverride If true, HTTP headers are taken into account + * + * @return string + * + * @since 1.6.0 + */ + public static function getIP($allowOverride = null) + { + // @todo Remove this block in 2.0 and change the parameter's default value from null to false + if ($allowOverride === null) + { + $allowOverride = self::$allowIpOverrides; + } + + return static::detectAndCleanIP($allowOverride); + } + + /** + * Set the IP address of the current visitor + * + * @param string $ip The visitor's IP address + * + * @return void + * + * @since 1.6.0 + * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. + */ + public static function setIP($ip) + { + self::$ip = $ip; + } + + /** + * Is it an IPv6 IP address? + * + * @param string $ip An IPv4 or IPv6 address + * + * @return boolean + * + * @since 1.6.0 + */ + public static function isIPv6($ip) + { + return filter_var(trim($ip), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; + } + + /** + * Checks if an IP is contained in a list of IPs or IP expressions + * + * @param string $ip The IPv4/IPv6 address to check + * @param array|string $ipRanges A comma-separated list or array of IP ranges to check against. + * Range may be specified as from-to, CIDR or IP with netmask. + * + * @return boolean + * @deprecated 2.0 Use IpHelper::isInRanges() instead + * @since 1.6.0 + */ + public static function IPinList($ip, $ipRanges = '') + { + return self::isInRanges($ip, $ipRanges); + } + + /** + * Checks if an IP is contained in a list of IPs or IP expressions + * + * @param string $ip The IPv4/IPv6 address to check + * @param array|string $ipRanges A comma-separated list or array of IP ranges to check against. + * Range may be specified as from-to, CIDR or IP with netmask. + * + * @return boolean + * + * @since __DEPLOY_VERSION__ + */ + public static function isInRanges($ip, $ipRanges) + { + // Reject empty IPs or ANY_ADDRESS + if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') + { + return false; + } + + // IP can not be in an empty range + if (empty($ipRanges)) + { + return false; + } + + // If the IP list is provided as string, convert it to an array + if (!\is_array($ipRanges)) + { + $ipRanges = preg_split('~,\s*~', $ipRanges); + } + + $ipRanges = array_reduce( + $ipRanges, + function ($list, $range) { + $range = trim($range); + + if (!empty($range)) + { + $list[] = $range; + } + + return $list; + }, + array() + ); + + foreach ($ipRanges as $ipRange) + { + if (self::isInRange($ip, $ipRange)) + { + return true; + } + } + + return false; + } + + /** + * Check if an IP is in a given range + * + * @param string $ip The IP to check + * @param string $ipRange The IP range; may be specified as from-to, CIDR or IP with netmask. + * + * @return boolean + */ + private static function isInRange($ip, $ipRange) + { + // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 + if (strpos($ipRange, '-') !== false) + { + list($from, $to) = preg_split('~\s*-\s*~', $ipRange, 2); + + return self::isInExplicitRange($ip, $from, $to); + } + + // Netmask or CIDR provided + if (strpos($ipRange, '/') !== false) + { + list($net, $mask) = explode('/', $ipRange, 2); + + // CIDR + if (is_numeric($mask)) + { + return self::isInCidrRange($ip, $net, $mask); + } + + // Netmask + return self::isInNetmaskRange($ip, $net, $mask); + } + + // Partial IP address, i.e. 123.[123.[123.]] + if (!self::isIPv6($ip) && preg_match('~\.$~', $ipRange)) + { + $segments = explode('.', $ipRange); + + // Drop empty segment + array_pop($segments); + + if (count($segments) > 3) + { + return false; + } + + $mask = count($segments) * 8; + + while (count($segments) < 4) + { + $segments[] = 0; + } + + $prefix = implode('.', $segments); + + return self::isInCidrRange($ip, $prefix, $mask); + } + + // Range is a single IP + $binaryIp = self::toBits($ip); + $binaryRange = self::toBits($ipRange); + + if (empty($binaryIp) || empty($binaryRange)) + { + return false; + } + + return $binaryIp === $binaryRange; + } + + /** + * Works around the REMOTE_ADDR not containing the user's IP + * + * @return void + * + * @since 1.6.0 + * @codeCoverageIgnore + * @deprecated 2.0 No replacement, this is never used + */ + public static function workaroundIPIssues() + { + $ip = static::getIP(); + + if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === $ip) + { + return; + } + + if (isset($_SERVER['REMOTE_ADDR'])) + { + $_SERVER['JOOMLA_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; + } + elseif (getenv('REMOTE_ADDR')) + { + $_SERVER['JOOMLA_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); + } + + $_SERVER['REMOTE_ADDR'] = $ip; + } + + /** + * Should I allow the remote client's IP to be overridden by an X-Forwarded-For or Client-Ip HTTP header? + * + * @param boolean $newState True to allow the override + * + * @return void + * + * @since 1.6.0 + * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. + */ + public static function setAllowIpOverrides($newState) + { + self::$allowIpOverrides = (bool) $newState; + } + + /** + * Get the visitor's IP address. + * + * Automatically handles reverse proxies reporting the IPs of intermediate devices, like load balancers. Examples: + * + * - https://stackoverflow.com/questions/2422395/why-is-request-envremote-addr-returning-two-ips + * + * The solution used is assuming that the last IP address is the external one. + * + * @param boolean $allowOverride If true, HTTP headers are taken into account + * + * @return string The validated IP address as provided. + * If no IP is available, an empty string is returned. + * + * @since 1.6.0 + */ + private static function detectAndCleanIP($allowOverride) + { + $rawIp = static::detectIP($allowOverride); + $ipList = preg_split('~,\s*~', $rawIp); + + $ipList = array_reduce( + $ipList, + function ($list, $ip) { + $ip = filter_var(trim($ip), FILTER_VALIDATE_IP); + + if ($ip !== false) + { + $list[] = $ip; + } + + return $list; + }, + array() + ); + + return (string) array_pop($ipList); + } + + /** + * Gets the visitor's IP address + * + * @param boolean $allowOverride If true, HTTP headers are taken into account + * + * @return string The IP address(es) as provided without validation. + * If no IP is available, an empty string is returned. + * + * @since 1.6.0 + */ + private static function detectIP($allowOverride) + { + // Order matters! + $indexes = array( + 'REMOTE_ADDR', + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + ); + + if (!$allowOverride) + { + return ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); + } + + $ip = getenv('REMOTE_ADDR'); + + foreach ($indexes as $index) + { + $ip = ArrayHelper::getValue($_SERVER, $index, $ip); + } + + return $ip; + } + + /** + * Converts IP address to bits string + * + * @param string $ip The IPv4 or IPv6 address + * + * @return string + * + * @since 1.6.0 + */ + private static function toBits($ip) + { + $packedIp = @inet_pton($ip); + + if ($packedIp === false) + { + return ''; + } + + $length = self::isIPv6($ip) ? 16 : 4; + $unpacked = unpack('A' . $length, $packedIp); + $unpacked = str_split($unpacked[1]); + $binaryIp = ''; + + foreach ($unpacked as $char) + { + $binaryIp .= str_pad(decbin(\ord($char)), 8, '0', STR_PAD_LEFT); + } + + $binaryIp = str_pad($binaryIp, $length * 8, '0', STR_PAD_RIGHT); + + return $binaryIp; + } + + /** + * Check if two IP addresses have the same IP format + * + * @param string $ip1 The first IP address + * @param string $ip2 The second IP address + * + * @return boolean + */ + private static function ipVersionMatch($ip1, $ip2) + { + return self::isIPv6($ip1) === self::isIPv6($ip2); + } + + /** + * @param string $ip The IP address to check + * @param string $from Lower bound of the range + * @param string $to Upper bound of the range + * + * @return boolean + */ + private static function isInExplicitRange($ip, $from, $to) + { + if (!self::ipVersionMatch($ip, $from) || !self::ipVersionMatch($ip, $to)) + { + return false; + } + + $binaryFrom = self::toBits($from); + $binaryTo = self::toBits($to); + $binaryIp = self::toBits($ip); + + if (empty($binaryFrom) || empty($binaryTo) || empty($binaryIp)) + { + return false; + } + + // Swap from/to if they're in the wrong order + if ($binaryFrom > $binaryTo) + { + list($binaryFrom, $binaryTo) = array($binaryTo, $binaryFrom); + } + + return $binaryFrom <= $binaryIp && $binaryIp <= $binaryTo; + } + + /** + * @param string $ip The IP address to check + * @param string $prefix The prefix address + * @param integer $mask The length of the prefix + * + * @return boolean + */ + private static function isInCidrRange($ip, $prefix, $mask) + { + if (!self::ipVersionMatch($ip, $prefix)) + { + return false; + } + + $binaryIp = static::toBits($ip); + $binaryPrefix = static::toBits($prefix); + + if (empty($binaryIp) || empty($binaryPrefix)) + { + return false; + } + + $maskedIp = substr($binaryIp, 0, $mask); + $maskedPrefix = substr($binaryPrefix, 0, $mask); + + return $maskedIp === $maskedPrefix; + } + + /** + * @param string $ip The IP address to check + * @param string $prefix The prefix address + * @param string $netmask The netmask + * + * @return boolean + */ + private static function isInNetmaskRange($ip, $prefix, $netmask) + { + $binaryMask = self::toBits($netmask); + + if (empty($binaryMask)) + { + return false; + } + + $mask = strlen(str_replace('0', '', $binaryMask)); + + return self::isInCidrRange($ip, $prefix, $mask); + } }