From 88e3d56aad5b33c7a854e2d2bfde749557bcb70e Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Wed, 31 Mar 2021 18:44:44 +0200 Subject: [PATCH 01/16] Tests - Add tests for IpHelper --- .gitignore | 1 + Tests/IpHelperTest.php | 263 +++++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- src/IpHelper.php | 2 +- 4 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 Tests/IpHelperTest.php diff --git a/.gitignore b/.gitignore index fe5e21c9..698dc8ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ composer.lock phpunit.xml /.phpunit.result.cache /build/ +/work/ diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php new file mode 100644 index 00000000..e0530ea5 --- /dev/null +++ b/Tests/IpHelperTest.php @@ -0,0 +1,263 @@ +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), + + '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), + ); + } + + /** + * @param string $ip + * @param string $ipTable + * @param boolean $expected + * + * @dataProvider sampleIPsWithTable + */ + public function testIpInList($ip, $ipTable, $expected) + { + $this->assertEquals($expected, IpHelper::IPinList($ip, $ipTable)); + } +} diff --git a/composer.json b/composer.json index 855d7237..e48a9e43 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ }, "require-dev": { "joomla/coding-standards": "~2.0@alpha", - "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0|^7.0|^8.0" + "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0|^7.0" }, "autoload": { "psr-4": { diff --git a/src/IpHelper.php b/src/IpHelper.php index b66758a1..d69c8707 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -149,7 +149,7 @@ public static function IPinList($ip, $ipTable = '') // Sanity check if (!\function_exists('inet_pton')) { - return false; + return false; // @codeCoverageIgnore } // Get the IP's in_adds representation From 78d9c5353a71186108f0251d564c2508dc09d66e Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 13:42:52 +0200 Subject: [PATCH 02/16] Tests - Add test cases for partial invalid network range --- Tests/IpHelperTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php index e0530ea5..9122cdd8 100644 --- a/Tests/IpHelperTest.php +++ b/Tests/IpHelperTest.php @@ -218,6 +218,7 @@ public function sampleIPsWithTable() '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), @@ -246,6 +247,7 @@ public function sampleIPsWithTable() '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), ); } From a53173f8bc33109a62561c5288c0a83bb8e8c53d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 14:21:52 +0200 Subject: [PATCH 03/16] Tests - Remove code coverage directive --- src/IpHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index d69c8707..b66758a1 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -149,7 +149,7 @@ public static function IPinList($ip, $ipTable = '') // Sanity check if (!\function_exists('inet_pton')) { - return false; // @codeCoverageIgnore + return false; } // Get the IP's in_adds representation From 0c9b91da0c7c636b384501022a67d7be56ad8375 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 14:46:00 +0200 Subject: [PATCH 04/16] Refactoring - Deprecate IP cache, which made IpHelper a Singleton --- src/IpHelper.php | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index b66758a1..4042154a 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -9,22 +9,20 @@ namespace Joomla\Utilities; /** - * IpHelper is a utility class for processing IP addresses - * - * This class is adapted from the `FOFUtilsIp` class distributed with the Joomla! CMS as part of the FOF library by Akeeba Ltd. - * The original class is copyright of Nicholas K. Dionysopoulos / Akeeba Ltd. + * 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 + * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ - private static $ip = null; + private static $ip; /** * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? @@ -35,15 +33,6 @@ final class IpHelper */ private static $allowIpOverrides = true; - /** - * Private constructor to prevent instantiation of this class - * - * @since 1.6.0 - */ - private function __construct() - { - } - /** * Get the current visitor's IP address * @@ -53,24 +42,19 @@ private function __construct() */ 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')) { - $ip = static::detectAndCleanIP(); + $myIP = @inet_pton($ip); - if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) + if ($myIP !== false) { - $myIP = @inet_pton($ip); - - if ($myIP !== false) - { - $ip = inet_ntop($myIP); - } + $ip = inet_ntop($myIP); } - - static::setIp($ip); } - return self::$ip; + return $ip; } /** @@ -81,6 +65,7 @@ public static function getIp() * @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) { @@ -383,6 +368,7 @@ public static function IPinList($ip, $ipTable = '') * @return void * * @since 1.6.0 + * @deprecated 2.0 No replacement, this is never used */ public static function workaroundIPIssues() { From bbd587f53831a3090f5d1039b0759d878a80641d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 14:54:18 +0200 Subject: [PATCH 05/16] Refactoring - Deprecate global allowOverride, which made IpHelper a Singleton --- src/IpHelper.php | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 4042154a..bc2131fc 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -29,7 +29,7 @@ abstract class IpHelper * * @var boolean * @since 1.6.0 - * @note The default value is false in version 2.0+ + * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ private static $allowIpOverrides = true; @@ -40,9 +40,14 @@ abstract class IpHelper * * @since 1.6.0 */ - public static function getIp() + public static function getIp($allowOverride = null) { - $ip = static::detectAndCleanIP(); + // Remove this block in 2.0 and change the parameter's default value from null to false + if ($allowOverride === null) { + $allowOverride = self::$allowIpOverrides; + } + + $ip = static::detectAndCleanIP($allowOverride); if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) { @@ -402,10 +407,11 @@ public static function workaroundIPIssues() * @return void * * @since 1.6.0 + * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = $newState ? true : false; + self::$allowIpOverrides = (bool) $newState; } /** @@ -418,13 +424,15 @@ public static function setAllowIpOverrides($newState) * * The solution used is assuming that the last IP address is the external one. * + * @param boolean $allowOverride + * * @return string * * @since 1.6.0 */ - protected static function detectAndCleanIP() + protected static function detectAndCleanIP($allowOverride) { - $ip = static::detectIP(); + $ip = static::detectIP($allowOverride); if (strstr($ip, ',') !== false || strstr($ip, ' ') !== false) { @@ -450,23 +458,25 @@ protected static function detectAndCleanIP() /** * Gets the visitor's IP address * + * @param boolean $allowOverride + * * @return string * * @since 1.6.0 */ - protected static function detectIP() + protected static function detectIP($allowOverride) { // 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'])) + if ($allowOverride && 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'])) + if ($allowOverride && isset($_SERVER['HTTP_CLIENT_IP'])) { return $_SERVER['HTTP_CLIENT_IP']; } @@ -488,13 +498,13 @@ protected static function detectIP() } // Do we have an x-forwarded-for HTTP header? - if (self::$allowIpOverrides && getenv('HTTP_X_FORWARDED_FOR')) + if ($allowOverride && 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')) + if ($allowOverride && getenv('HTTP_CLIENT_IP')) { return getenv('HTTP_CLIENT_IP'); } From 87e0b9e6106e14ab345e287727fb9fe45fd54f2d Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 15:01:39 +0200 Subject: [PATCH 06/16] Refactoring - Remove check for existence of inet_ntop and/or inet_pton - they are always present since PHP 5.1 --- src/IpHelper.php | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index bc2131fc..27f93f86 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -36,6 +36,8 @@ abstract class IpHelper /** * Get the current visitor's IP address * + * @param boolean|null $allowOverride If true, HTTP headers are taken into account + * * @return string * * @since 1.6.0 @@ -49,7 +51,7 @@ public static function getIp($allowOverride = null) $ip = static::detectAndCleanIP($allowOverride); - if (!empty($ip) && ($ip != '0.0.0.0') && \function_exists('inet_pton') && \function_exists('inet_ntop')) + if (!empty($ip) && $ip != '0.0.0.0') { $myIP = @inet_pton($ip); @@ -136,12 +138,6 @@ public static function IPinList($ip, $ipTable = '') return false; } - // Sanity check - if (!\function_exists('inet_pton')) - { - return false; - } - // Get the IP's in_adds representation $myIP = @inet_pton($ip); From 2e0ad6edafd4379be54574d56448b2ac2c2ab8bc Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 15:05:22 +0200 Subject: [PATCH 07/16] Refactoring - Remove check for existence of getenv - it is always present since PHP 4 --- src/IpHelper.php | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 27f93f86..33ca6227 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -384,12 +384,9 @@ public static function workaroundIPIssues() { $_SERVER['JOOMLA_REMOTE_ADDR'] = $_SERVER['REMOTE_ADDR']; } - elseif (\function_exists('getenv')) + elseif (getenv('REMOTE_ADDR')) { - if (getenv('REMOTE_ADDR')) - { - $_SERVER['JOOMLA_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); - } + $_SERVER['JOOMLA_REMOTE_ADDR'] = getenv('REMOTE_ADDR'); } $_SERVER['REMOTE_ADDR'] = $ip; @@ -484,15 +481,6 @@ protected static function detectIP($allowOverride) } } - /* - * 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 ($allowOverride && getenv('HTTP_X_FORWARDED_FOR')) { From fe93af7de071186047a56bd0d280518dcf2b158c Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 15:09:21 +0200 Subject: [PATCH 08/16] Refactoring - Use strpos instead of strstr (saves memory), use strict comparision --- src/IpHelper.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 33ca6227..8c5ab75a 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -51,7 +51,7 @@ public static function getIp($allowOverride = null) $ip = static::detectAndCleanIP($allowOverride); - if (!empty($ip) && $ip != '0.0.0.0') + if (!empty($ip) && $ip !== '0.0.0.0') { $myIP = @inet_pton($ip); @@ -154,7 +154,7 @@ public static function IPinList($ip, $ipTable = '') $ipExpression = trim($ipExpression); // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 - if (strstr($ipExpression, '-')) + if (strpos($ipExpression, '-') !== false) { list($from, $to) = explode('-', $ipExpression, 2); @@ -191,7 +191,7 @@ public static function IPinList($ip, $ipTable = '') } } // Netmask or CIDR provided - elseif (strstr($ipExpression, '/')) + elseif (strpos($ipExpression, '/') !== false) { $binaryip = static::inetToBits($myIP); @@ -209,7 +209,7 @@ public static function IPinList($ip, $ipTable = '') continue; } - if ($ipv6 && strstr($maskbits, ':')) + if ($ipv6 && strpos($maskbits, ':') !== false) { // Perform an IPv6 CIDR check if (static::checkIPv6CIDR($myIP, $ipExpression)) @@ -221,7 +221,7 @@ public static function IPinList($ip, $ipTable = '') continue; } - if (!$ipv6 && strstr($maskbits, '.')) + if (!$ipv6 && strpos($maskbits, '.') !== false) { // Convert IPv4 netmask to CIDR $long = ip2long($maskbits); @@ -270,7 +270,7 @@ public static function IPinList($ip, $ipTable = '') continue; } - if ($ipCheck == $myIP) + if ($ipCheck === $myIP) { return true; } @@ -280,12 +280,12 @@ public static function IPinList($ip, $ipTable = '') // 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) == '.') + if (substr($ipExpression, -1) === '.') { // Partial IP address. Convert to CIDR and re-match foreach (count_chars($ipExpression, 1) as $i => $val) { - if ($i == 46) + if ($i === 46) { $dots = $val; } @@ -351,7 +351,7 @@ public static function IPinList($ip, $ipTable = '') { $ip = @inet_pton(trim($ipExpression)); - if ($ip == $myIP) + if ($ip === $myIP) { return true; } @@ -427,7 +427,7 @@ protected static function detectAndCleanIP($allowOverride) { $ip = static::detectIP($allowOverride); - if (strstr($ip, ',') !== false || strstr($ip, ' ') !== false) + if (strpos($ip, ',') !== false || strpos($ip, ' ') !== false) { $ip = str_replace(' ', ',', $ip); $ip = str_replace(',,', ',', $ip); @@ -514,7 +514,7 @@ protected static function detectIP($allowOverride) */ protected static function inetToBits($inet) { - if (\strlen($inet) == 4) + if (\strlen($inet) === 4) { $unpacked = unpack('A4', $inet); } From 55f49b1806bc70360a406f4f5375490e02522b8a Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 16:38:49 +0200 Subject: [PATCH 09/16] Refactoring - Simplify and harden IP detection --- src/IpHelper.php | 288 +++++++++++++++++++++-------------------------- 1 file changed, 129 insertions(+), 159 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 8c5ab75a..6509d59a 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -19,7 +19,7 @@ abstract class IpHelper * The IP address of the current visitor * * @var string - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 If you want to cache the IP address, you should handle that yourself. */ private static $ip; @@ -28,7 +28,7 @@ abstract class IpHelper * Should I allow IP overrides through X-Forwarded-For or Client-Ip HTTP headers? * * @var boolean - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ private static $allowIpOverrides = true; @@ -36,7 +36,7 @@ abstract class IpHelper /** * Get the current visitor's IP address * - * @param boolean|null $allowOverride If true, HTTP headers are taken into account + * @param boolean $allowOverride If true, HTTP headers are taken into account * * @return string * @@ -44,24 +44,13 @@ abstract class IpHelper */ public static function getIp($allowOverride = null) { - // Remove this block in 2.0 and change the parameter's default value from null to false - if ($allowOverride === null) { - $allowOverride = self::$allowIpOverrides; - } - - $ip = static::detectAndCleanIP($allowOverride); - - if (!empty($ip) && $ip !== '0.0.0.0') + // @todo Remove this block in 2.0 and change the parameter's default value from null to false + if ($allowOverride === null) { - $myIP = @inet_pton($ip); - - if ($myIP !== false) - { - $ip = inet_ntop($myIP); - } + $allowOverride = self::$allowIpOverrides; } - return $ip; + return static::detectAndCleanIP($allowOverride); } /** @@ -71,7 +60,7 @@ public static function getIp($allowOverride = null) * * @return void * - * @since 1.6.0 + * @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) @@ -82,7 +71,7 @@ public static function setIp($ip) /** * Is it an IPv6 IP address? * - * @param string $ip An IPv4 or IPv6 address + * @param string $ip An IPv4 or IPv6 address * * @return boolean * @@ -90,7 +79,7 @@ public static function setIp($ip) */ public static function isIPv6($ip) { - return strpos($ip, ':') !== false; + return filter_var(trim($ip), FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; } /** @@ -251,110 +240,112 @@ public static function IPinList($ip, $ipTable = '') return true; } } - else + elseif ($ipv6) { // IPv6: Only single IPs are supported - if ($ipv6) - { - $ipExpression = trim($ipExpression); + $ipExpression = trim($ipExpression); - if (!static::isIPv6($ipExpression)) - { - continue; - } - - $ipCheck = @inet_pton($ipExpression); + if (!static::isIPv6($ipExpression)) + { + continue; + } - if ($ipCheck === false) - { - continue; - } + $ipCheck = @inet_pton($ipExpression); - if ($ipCheck === $myIP) - { - return true; - } + if ($ipCheck === false) + { + continue; } - else + + if ($ipCheck === $myIP) { - // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] - $dots = 0; + 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) === '.') + if (substr($ipExpression, -1) === '.') + { + // Partial IP address. Convert to CIDR and re-match + foreach (count_chars($ipExpression, 1) as $i => $val) { - // Partial IP address. Convert to CIDR and re-match - foreach (count_chars($ipExpression, 1) as $i => $val) + if ($i === 46) { - if ($i === 46) - { - $dots = $val; - } + $dots = $val; } + } - switch ($dots) - { - case 1: - $netmask = '255.0.0.0'; - $ipExpression .= '0.0.0'; + switch ($dots) + { + case 1: + $netmask = '255.0.0.0'; + $ipExpression .= '0.0.0'; - break; + break; - case 2: - $netmask = '255.255.0.0'; - $ipExpression .= '0.0'; + case 2: + $netmask = '255.255.0.0'; + $ipExpression .= '0.0'; - break; + break; - case 3: - $netmask = '255.255.255.0'; - $ipExpression .= '0'; + case 3: + $netmask = '255.255.255.0'; + $ipExpression .= '0'; - break; + break; - default: - $dots = 0; - } + default: + $dots = 0; + } - if ($dots) - { - $binaryip = static::inetToBits($myIP); + 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); + // Convert netmask to CIDR + $long = ip2long($netmask); + $base = ip2long('255.255.255.255'); + $maskbits = 32 - log(($long ^ $base) + 1, 2); - $net = @inet_pton($ipExpression); + $net = @inet_pton($ipExpression); - // Sanity check - if ($net === false) - { - continue; - } + // 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); + // 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); + // 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 ($ipNetBits === $netBits) + { + return true; } } + } - if (!$dots) - { - $ip = @inet_pton(trim($ipExpression)); + if (!$dots) + { + $ip = @inet_pton(trim($ipExpression)); - if ($ip === $myIP) - { - return true; - } + if ($ip === $myIP) + { + return true; } } } @@ -368,7 +359,7 @@ public static function IPinList($ip, $ipTable = '') * * @return void * - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 No replacement, this is never used */ public static function workaroundIPIssues() @@ -399,53 +390,51 @@ public static function workaroundIPIssues() * * @return void * - * @since 1.6.0 + * @since 1.6.0 * @deprecated 2.0 Use the parameter of IpHelper::getIp() instead. */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool) $newState; + self::$allowIpOverrides = (bool)$newState; } /** - * Gets the visitor's IP address. + * Get 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. * * @param boolean $allowOverride * - * @return string + * @return string The validated IP address as provided. + * If no IP is available, an empty string is returned. * * @since 1.6.0 */ protected static function detectAndCleanIP($allowOverride) { - $ip = static::detectIP($allowOverride); + $rawIp = static::detectIP($allowOverride); + $ipList = preg_split('~,\s*~', $rawIp); - if (strpos($ip, ',') !== false || strpos($ip, ' ') !== false) - { - $ip = str_replace(' ', ',', $ip); - $ip = str_replace(',,', ',', $ip); - $ips = explode(',', $ip); - $ip = ''; + $ipList = array_reduce( + $ipList, + function ($list, $ip) { + $ip = filter_var(trim($ip), FILTER_VALIDATE_IP); - while (empty($ip) && !empty($ips)) - { - $ip = array_pop($ips); - $ip = trim($ip); - } - } - else - { - $ip = trim($ip); - } + if ($ip !== false) + { + $list[] = $ip; + } - return $ip; + return $list; + }, + array() + ); + + return (string) array_pop($ipList); } /** @@ -453,54 +442,35 @@ protected static function detectAndCleanIP($allowOverride) * * @param boolean $allowOverride * - * @return string + * @return string The IP address(es) as provided without validation. + * If no IP is available, an empty string is returned. * * @since 1.6.0 */ protected static function detectIP($allowOverride) { - // Normally the $_SERVER superglobal is set - if (isset($_SERVER)) + // Order matters! + $indexes = array( + 'REMOTE_ADDR', + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + ); + + if (!$allowOverride) { - // Do we have an x-forwarded-for HTTP header (e.g. NginX)? - if ($allowOverride && 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 ($allowOverride && 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']; - } + $ip = ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); } - - // Do we have an x-forwarded-for HTTP header? - if ($allowOverride && getenv('HTTP_X_FORWARDED_FOR')) - { - return getenv('HTTP_X_FORWARDED_FOR'); - } - - // Do we have a client-ip header? - if ($allowOverride && getenv('HTTP_CLIENT_IP')) + else { - return getenv('HTTP_CLIENT_IP'); - } + $ip = ''; - // Normal, non-proxied server or server behind a transparent proxy - if (getenv('REMOTE_ADDR')) - { - return getenv('REMOTE_ADDR'); + foreach ($indexes as $index) + { + $ip = ArrayHelper::getValue($_SERVER, $index, $ip); + } } - // Catch-all case for broken servers, apparently - return ''; + return $ip; } /** @@ -550,8 +520,8 @@ protected static function checkIPv6CIDR($ip, $cidrnet) $binaryip = static::inetToBits($ip); list($net, $maskbits) = explode('/', $cidrnet); - $net = inet_pton($net); - $binarynet = static::inetToBits($net); + $net = inet_pton($net); + $binarynet = static::inetToBits($net); $ipNetBits = substr($binaryip, 0, $maskbits); $netBits = substr($binarynet, 0, $maskbits); From bdcb305c2fa4ea1231b8a525ba5aa55bacdc0edb Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 20:00:34 +0200 Subject: [PATCH 10/16] Refactoring - Complete refactoring --- Tests/IpHelperTest.php | 10 +- src/IpHelper.php | 435 +++++++++++++++++------------------------ 2 files changed, 182 insertions(+), 263 deletions(-) diff --git a/Tests/IpHelperTest.php b/Tests/IpHelperTest.php index 9122cdd8..ba4b025e 100644 --- a/Tests/IpHelperTest.php +++ b/Tests/IpHelperTest.php @@ -59,7 +59,7 @@ protected function setUp() unset($_SERVER['HTTP_FORWARDED']); unset($_SERVER['REMOTE_ADDR']); - IpHelper::setIp(null); + IpHelper::setIP(null); } /** @@ -130,10 +130,10 @@ public function testGetIpFromServerWithOverride($index, $ip, $normalised) { $_SERVER[$index] = $ip; - IpHelper::setIp(null); + IpHelper::setIP(null); IpHelper::setAllowIpOverrides(true); - $this->assertEquals($normalised, IpHelper::getIp()); + $this->assertEquals($normalised, IpHelper::getIP()); } /** @@ -152,7 +152,7 @@ public function testGetIpFromServerWithoutOverride($index, $ip, $normalised) IpHelper::setAllowIpOverrides(false); - $this->assertEquals('80.80.80.80', IpHelper::getIp()); + $this->assertEquals('80.80.80.80', IpHelper::getIP()); } /** @@ -260,6 +260,6 @@ public function sampleIPsWithTable() */ public function testIpInList($ip, $ipTable, $expected) { - $this->assertEquals($expected, IpHelper::IPinList($ip, $ipTable)); + $this->assertEquals($expected, IpHelper::isInRanges($ip, $ipTable)); } } diff --git a/src/IpHelper.php b/src/IpHelper.php index 6509d59a..267d4d9e 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -42,7 +42,7 @@ abstract class IpHelper * * @since 1.6.0 */ - public static function getIp($allowOverride = null) + 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) @@ -63,7 +63,7 @@ public static function getIp($allowOverride = null) * @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) + public static function setIP($ip) { self::$ip = $ip; } @@ -85,273 +85,120 @@ public static function isIPv6($ip) /** * 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 + * @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 1.6.0 */ - public static function IPinList($ip, $ipTable = '') + public static function isInRanges($ip, $ipRanges = '') { - // No point proceeding with an empty IP list - if (empty($ipTable)) + // Reject empty IPs or ANY_ADDRESS + if (empty($ip) || $ip === '0.0.0.0' || $ip === '::') { 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 = array($ipTable); - } - } - - // If no IP address is found, return false - if ($ip === '0.0.0.0') + // IP can not be in an empty range + if (empty($ipRanges)) { return false; } - // If no IP is given, return false - if (empty($ip)) + // If the IP list is provided as string, convert it to an array + if (!\is_array($ipRanges)) { - return false; + $ipRanges = preg_split('~,\s*~', $ipRanges); } - // 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 (strpos($ipExpression, '-') !== false) - { - list($from, $to) = explode('-', $ipExpression, 2); + $ipRanges = array_reduce( + $ipRanges, + function ($list, $range) { + $range = trim($range); - if ($ipv6 && (!static::isIPv6($from) || !static::isIPv6($to))) + if (!empty($range)) { - // Do not apply IPv4 filtering on an IPv6 address - continue; + $list[] = $range; } - 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) = array($to, $from); - } + return $list; + }, + array() + ); - if (($myIP >= $from) && ($myIP <= $to)) - { - return true; - } - } - // Netmask or CIDR provided - elseif (strpos($ipExpression, '/') !== false) + foreach ($ipRanges as $ipRange) + { + if (self::isInRange($ip, $ipRange)) { - $binaryip = static::inetToBits($myIP); - - list($net, $maskbits) = explode('/', $ipExpression, 2); + return true; + } + } - if ($ipv6 && !static::isIPv6($net)) - { - // Do not apply IPv4 filtering on an IPv6 address - continue; - } + return false; + } - if (!$ipv6 && static::isIPv6($net)) - { - // Do not apply IPv6 filtering on an IPv4 address - continue; - } + 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); - if ($ipv6 && strpos($maskbits, ':') !== false) - { - // Perform an IPv6 CIDR check - if (static::checkIPv6CIDR($myIP, $ipExpression)) - { - return true; - } - - // If we didn't match it proceed to the next expression - continue; - } + return self::isInExplicitRange($ip, $from, $to); + } - if (!$ipv6 && strpos($maskbits, '.') !== false) - { - // Convert IPv4 netmask to CIDR - $long = ip2long($maskbits); - $base = ip2long('255.255.255.255'); - $maskbits = 32 - log(($long ^ $base) + 1, 2); - } + // Netmask or CIDR provided + if (strpos($ipRange, '/') !== false) + { + list($net, $mask) = explode('/', $ipRange, 2); - // Convert network IP to in_addr representation - $net = @inet_pton($net); + // CIDR + if (is_numeric($mask)) + { + return self::isInCidrRange($ip, $net, $mask); + } - // Sanity check - if ($net === false) - { - continue; - } + // Netmask + return self::isInNetmaskRange($ip, $net, $mask); + } - // Get the network's binary representation - $expectedNumberOfBits = $ipv6 ? 128 : 24; - $binarynet = str_pad(static::inetToBits($net), $expectedNumberOfBits, '0', STR_PAD_RIGHT); + // Partial IP address, i.e. 123.[123.[123.]] + if (!self::isIPv6($ip) && preg_match('~\.$~', $ipRange)) + { + $segments = explode('.', $ipRange); - // Check the corresponding bits of the IP and the network - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); + // Drop empty segment + array_pop($segments); - if ($ipNetBits === $netBits) - { - return true; - } - } - elseif ($ipv6) + if (count($segments) > 3) { - // IPv6: Only single IPs are supported - $ipExpression = trim($ipExpression); - - if (!static::isIPv6($ipExpression)) - { - continue; - } - - $ipCheck = @inet_pton($ipExpression); + return false; + } - if ($ipCheck === false) - { - continue; - } + $mask = count($segments) * 8; - if ($ipCheck === $myIP) - { - return true; - } - } - else + while (count($segments) < 4) { - // Standard IPv4 address, i.e. 123.123.123.123 or partial IP address, i.e. 123.[123.][123.][123] - $dots = 0; + $segments[] = 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; - } - } - } + $prefix = implode('.', $segments); - if (!$dots) - { - $ip = @inet_pton(trim($ipExpression)); + return self::isInCidrRange($ip, $prefix, $mask); + } - if ($ip === $myIP) - { - return true; - } - } - } + // Range is a single IP + $binaryIp = self::toBits($ip); + $binaryRange = self::toBits($ipRange); + + if (empty($binaryIp) || empty($binaryRange)) + { + return false; } - return false; + return $binaryIp === $binaryRange; } /** @@ -360,11 +207,12 @@ public static function IPinList($ip, $ipTable = '') * @return void * * @since 1.6.0 + * @codeCoverageIgnore * @deprecated 2.0 No replacement, this is never used */ public static function workaroundIPIssues() { - $ip = static::getIp(); + $ip = static::getIP(); if (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === $ip) { @@ -395,7 +243,7 @@ public static function workaroundIPIssues() */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool)$newState; + self::$allowIpOverrides = (bool) $newState; } /** @@ -414,9 +262,9 @@ public static function setAllowIpOverrides($newState) * * @since 1.6.0 */ - protected static function detectAndCleanIP($allowOverride) + private static function detectAndCleanIP($allowOverride) { - $rawIp = static::detectIP($allowOverride); + $rawIp = static::detectIP($allowOverride); $ipList = preg_split('~,\s*~', $rawIp); $ipList = array_reduce( @@ -434,7 +282,7 @@ function ($list, $ip) { array() ); - return (string) array_pop($ipList); + return (string)array_pop($ipList); } /** @@ -447,7 +295,7 @@ function ($list, $ip) { * * @since 1.6.0 */ - protected static function detectIP($allowOverride) + private static function detectIP($allowOverride) { // Order matters! $indexes = array( @@ -476,56 +324,127 @@ protected static function detectIP($allowOverride) /** * Converts inet_pton output to bits string * - * @param string $inet The in_addr representation of an IPv4 or IPv6 address + * @param string $ip The IPv4 or IPv6 address * * @return string * * @since 1.6.0 */ - protected static function inetToBits($inet) + private static function toBits($ip) { - if (\strlen($inet) === 4) - { - $unpacked = unpack('A4', $inet); - } - else + $packedIp = inet_pton($ip); + + if ($packedIp === false) { - $unpacked = unpack('A16', $inet); + return ''; } + $length = self::isIPv6($ip) ? 16 : 4; + $unpacked = unpack('A' . $length, $packedIp); $unpacked = str_split($unpacked[1]); - $binaryip = ''; + $binaryIp = ''; foreach ($unpacked as $char) { - $binaryip .= str_pad(decbin(\ord($char)), 8, '0', STR_PAD_LEFT); + $binaryIp .= str_pad(decbin(\ord($char)), 8, '0', STR_PAD_LEFT); } - return $binaryip; + $binaryIp = str_pad($binaryIp, $length * 8, '0', STR_PAD_RIGHT); + + return $binaryIp; } /** - * Checks if an IPv6 address $ip is part of the IPv6 CIDR block $cidrnet + * Check if two IP addresses have the same IP format * - * @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 + * @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 + * @param string $prefix + * @param integer $mask * - * @since 1.6.0 + * @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 + * @param string $prefix + * @param string $netmask + * + * @return boolean */ - protected static function checkIPv6CIDR($ip, $cidrnet) + private static function isInNetmaskRange($ip, $prefix, $netmask) { - $ip = inet_pton($ip); - $binaryip = static::inetToBits($ip); + $binaryMask = self::toBits($netmask); - list($net, $maskbits) = explode('/', $cidrnet); - $net = inet_pton($net); - $binarynet = static::inetToBits($net); + if (empty($binaryMask)) + { + return false; + } - $ipNetBits = substr($binaryip, 0, $maskbits); - $netBits = substr($binarynet, 0, $maskbits); + $mask = strlen(str_replace('0', '', $binaryMask)); - return $ipNetBits === $netBits; + return self::isInCidrRange($ip, $prefix, $mask); } } From cfabc8eae4e26363871265cbfab46bed09e26be1 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 20:03:58 +0200 Subject: [PATCH 11/16] Refactoring - Prevent inet_pton from emitting a warning --- src/IpHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 267d4d9e..e2bde8fb 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -322,7 +322,7 @@ private static function detectIP($allowOverride) } /** - * Converts inet_pton output to bits string + * Converts IP address to bits string * * @param string $ip The IPv4 or IPv6 address * @@ -332,7 +332,7 @@ private static function detectIP($allowOverride) */ private static function toBits($ip) { - $packedIp = inet_pton($ip); + $packedIp = @inet_pton($ip); if ($packedIp === false) { From 4a6c2871bcc05dcd6eaa345e4078288e0d7bab54 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 23:30:57 +0200 Subject: [PATCH 12/16] Style - Add comments --- src/IpHelper.php | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index e2bde8fb..45026464 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -139,6 +139,14 @@ function ($list, $range) { 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 bool + */ private static function isInRange($ip, $ipRange) { // Inclusive IP range, i.e. 123.123.123.123-124.125.126.127 @@ -243,7 +251,7 @@ public static function workaroundIPIssues() */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool) $newState; + self::$allowIpOverrides = (bool)$newState; } /** @@ -255,7 +263,7 @@ public static function setAllowIpOverrides($newState) * * The solution used is assuming that the last IP address is the external one. * - * @param boolean $allowOverride + * @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. @@ -282,13 +290,13 @@ function ($list, $ip) { array() ); - return (string)array_pop($ipList); + return (string) array_pop($ipList); } /** * Gets the visitor's IP address * - * @param boolean $allowOverride + * @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. @@ -400,9 +408,9 @@ private static function isInExplicitRange($ip, $from, $to) } /** - * @param string $ip - * @param string $prefix - * @param integer $mask + * @param string $ip The IP address to check + * @param string $prefix The prefix address + * @param integer $mask The length of the prefix * * @return boolean */ @@ -428,9 +436,9 @@ private static function isInCidrRange($ip, $prefix, $mask) } /** - * @param string $ip - * @param string $prefix - * @param string $netmask + * @param string $ip The IP address to check + * @param string $prefix The prefix address + * @param string $netmask The netmask * * @return boolean */ From fdc29aad8fbf822c3441a97ddc9f1531e08b356c Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 1 Apr 2021 23:43:49 +0200 Subject: [PATCH 13/16] Style - Add CS fixes --- src/IpHelper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 45026464..5793ad35 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -145,7 +145,7 @@ function ($list, $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 bool + * @return boolean */ private static function isInRange($ip, $ipRange) { @@ -251,7 +251,7 @@ public static function workaroundIPIssues() */ public static function setAllowIpOverrides($newState) { - self::$allowIpOverrides = (bool)$newState; + self::$allowIpOverrides = (bool) $newState; } /** From c62f29d009f2e0e0cd0478cfb7fdb80bd7b206ae Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Fri, 2 Apr 2021 03:52:07 +0200 Subject: [PATCH 14/16] Refactoring - Re-add IPinList() as proxy for isInRanges() --- src/IpHelper.php | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 5793ad35..66625ebb 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -90,10 +90,26 @@ public static function isIPv6($ip) * 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 isInRanges($ip, $ipRanges = '') + 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 === '::') From 119b8d0d1c405afe77ea948602d6214479539538 Mon Sep 17 00:00:00 2001 From: Niels Braczek Date: Thu, 8 Apr 2021 13:53:06 +0200 Subject: [PATCH 15/16] Refactoring - Follow early return pattern, allow REMOTE_ADDR be set in environment --- src/IpHelper.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/IpHelper.php b/src/IpHelper.php index 66625ebb..be0b9c6f 100644 --- a/src/IpHelper.php +++ b/src/IpHelper.php @@ -330,16 +330,14 @@ private static function detectIP($allowOverride) if (!$allowOverride) { - $ip = ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); + return ArrayHelper::getValue($_SERVER, 'REMOTE_ADDR', getenv('REMOTE_ADDR')); } - else - { - $ip = ''; - foreach ($indexes as $index) - { - $ip = ArrayHelper::getValue($_SERVER, $index, $ip); - } + $ip = getenv('REMOTE_ADDR'); + + foreach ($indexes as $index) + { + $ip = ArrayHelper::getValue($_SERVER, $index, $ip); } return $ip; From 81ff024194f264daaa1165b7cadca634312ab85b Mon Sep 17 00:00:00 2001 From: Hannes Papenberg Date: Wed, 9 Jul 2025 14:43:50 +0200 Subject: [PATCH 16/16] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0f5898cf..425fe5da 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,3 @@ phpunit.xml .idea/ /.phpunit.result.cache /.phpunit.cache/ -