diff --git a/composer.json b/composer.json index c3c027f5..1e960edf 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,7 @@ "ext-json": "*", "ext-openssl": "*", "ext-fileinfo": "*", + "ext-gmp": "*", "phpmailer/phpmailer": "~6.0", "monolog/monolog": "^1.24", "robthree/twofactorauth": "^1.6", diff --git a/install/lib/class.FroxlorInstall.php b/install/lib/class.FroxlorInstall.php index 057dcf5c..89d8d8bc 100644 --- a/install/lib/class.FroxlorInstall.php +++ b/install/lib/class.FroxlorInstall.php @@ -779,6 +779,8 @@ class FroxlorInstall $mysql_access_host_array[] = $this->_data['serverip']; } + $mysql_access_host_array = array_unique($mysql_access_host_array); + foreach ($mysql_access_host_array as $mysql_access_host) { $frox_db = str_replace('`', '', $this->_data['mysql_database']); $this->_grantDbPrivilegesTo($db_root, $frox_db, $this->_data['mysql_unpriv_user'], $this->_data['mysql_unpriv_pass'], $mysql_access_host); diff --git a/install/updates/froxlor/0.10/update_0.10.inc.php b/install/updates/froxlor/0.10/update_0.10.inc.php index 60a17e3a..9ff75148 100644 --- a/install/updates/froxlor/0.10/update_0.10.inc.php +++ b/install/updates/froxlor/0.10/update_0.10.inc.php @@ -1,7 +1,7 @@ fetch(\PDO::FETCH_ASSOC)) { - if (Validate::is_ipv6($iprow['ip'])) { + if (IPTools::is_ipv6($iprow['ip'])) { $ip = inet_ntop(inet_pton($iprow['ip'])); Database::pexecute($upd_stmt, [ 'ip' => $ip, diff --git a/lib/Froxlor/Api/FroxlorRPC.php b/lib/Froxlor/Api/FroxlorRPC.php index 9e89150c..d3faba45 100644 --- a/lib/Froxlor/Api/FroxlorRPC.php +++ b/lib/Froxlor/Api/FroxlorRPC.php @@ -3,6 +3,8 @@ namespace Froxlor\Api; use Exception; +use Froxlor\Database\Database; +use Froxlor\System\IPTools; /** * This file is part of the Froxlor project. @@ -60,11 +62,12 @@ class FroxlorRPC * * @param string $key * @param string $secret - * @return boolean + * + * @return bool */ private static function validateAuth(string $key, string $secret): bool { - $sel_stmt = \Froxlor\Database\Database::prepare( + $sel_stmt = Database::prepare( " SELECT ak.*, a.api_allowed as admin_api_allowed, c.api_allowed as cust_api_allowed, c.deactivated FROM `api_keys` ak @@ -73,7 +76,7 @@ class FroxlorRPC WHERE `apikey` = :ak AND `secret` = :as " ); - $result = \Froxlor\Database\Database::pexecute_first($sel_stmt, array( + $result = Database::pexecute_first($sel_stmt, array( 'ak' => $key, 'as' => $secret ), true, true); @@ -83,8 +86,7 @@ class FroxlorRPC if (!empty($result['allowed_from'])) { // @todo allow specification and validating of whole subnets later $ip_list = explode(",", $result['allowed_from']); - $access_ip = inet_ntop(inet_pton($_SERVER['REMOTE_ADDR'])); - if (in_array($access_ip, $ip_list)) { + if (self::validateAllowedFrom($ip_list, $_SERVER['REMOTE_ADDR'])) { return true; } } else { @@ -95,6 +97,32 @@ class FroxlorRPC throw new Exception('Invalid authorization credentials', 403); } + /** + * validate if given remote_addr is within the list of allowed ip/ip-ranges + * + * @param array $allowed_from + * @param string $remote_addr + * + * @return bool + */ + private static function validateAllowedFrom(array $allowed_from, string $remote_addr): bool + { + // shorten IP for comparison + $remote_addr = inet_ntop(inet_pton($remote_addr)); + // check for diret matches + if (in_array($remote_addr, $allowed_from)) { + return true; + } + // check for possible cidr ranges + foreach ($allowed_from as $ip) { + $ip_cidr = explode("/", $ip); + if (count($ip_cidr) == 2 && IPTools::ip_in_range($ip_cidr, $remote_addr)) { + return true; + } + } + return false; + } + /** * validates the given command * diff --git a/lib/Froxlor/Settings/Store.php b/lib/Froxlor/Settings/Store.php index d19328f1..a7efe3d7 100644 --- a/lib/Froxlor/Settings/Store.php +++ b/lib/Froxlor/Settings/Store.php @@ -266,7 +266,7 @@ class Store if (count($ip_cidr) === 2) { $ip = $ip_cidr[0]; if (strlen($ip_cidr[1]) <= 2) { - $ip_cidr[1] = \Froxlor\Validate\Validate::cidr2NetmaskAddr($org_ip); + $ip_cidr[1] = \Froxlor\System\IPTools::cidr2NetmaskAddr($org_ip); } $newfieldvalue[] = $ip . '/' . $ip_cidr[1]; } else { diff --git a/lib/Froxlor/System/IPTools.php b/lib/Froxlor/System/IPTools.php new file mode 100644 index 00000000..7ff2d81f --- /dev/null +++ b/lib/Froxlor/System/IPTools.php @@ -0,0 +1,165 @@ + ip, 1 => netmask in decimal, e.g. [0 => '123.123.123.123', 1 => 24] + * @param string $ip ip-address to check + * + * @return bool + */ + public static function ip_in_range(array $ip_cidr, string $ip): bool + { + $netip = $ip_cidr[0]; + if (self::is_ipv6($netip)) { + return self::ipv6_in_range($ip_cidr, $ip); + } + $netmask = $ip_cidr[1]; + $range_decimal = ip2long($netip); + $ip_decimal = ip2long($ip); + $wildcard_decimal = pow(2, (32 - $netmask)) - 1; + $netmask_decimal = ~$wildcard_decimal; + return (($ip_decimal & $netmask_decimal) == ($range_decimal & $netmask_decimal)); + } + + /** + * Checks whether the given ipv6 $ip is in range of given ip/cidr range + * + * @param array $ip_cidr 0 => ip, 1 => netmask in decimal, e.g. [0 => '123:123::1', 1 => 64] + * @param string $ip ip-address to check + * + * @return bool + */ + private static function ipv6_in_range(array $ip_cidr, string $ip): bool + { + $in_range = false; + + $size = 128 - $ip_cidr[1]; + if ($size == 0) { + return inet_ntop(inet_pton($ip_cidr[0])) == inet_ntop(inet_pton($ip)); + } + $addr = gmp_init('0x' . str_replace(':', '', self::inet6_expand($ip_cidr[0]))); + $mask = gmp_init('0x' . str_replace(':', '', self::inet6_expand(self::inet6_prefix_to_mask($ip_cidr[1])))); + $prefix = gmp_and($addr, $mask); + $start = gmp_strval(gmp_add($prefix, '0x1'), 16); + $end = '0b'; + for ($i = 0; $i < $size; $i++) { + $end .= '1'; + } + $end = gmp_strval(gmp_add($prefix, gmp_init($end)), 16); + $start_result = ''; + for ($i = 0; $i < 8; $i++) { + $start_result .= substr($start, $i * 4, 4); + if ($i != 7) $start_result .= ':'; + } + $end_result = ''; + for ($i = 0; $i < 8; $i++) { + $end_result .= substr($end, $i * 4, 4); + if ($i != 7) $end_result .= ':'; + } + + $first = self::ip2long6($start_result); + $last = self::ip2long6($end_result); + $ip = self::ip2long6($ip); + + $in_range = ($ip >= $first && $ip <= $last); + return $in_range; + } + + private static function ip2long6($ip) + { + $ip_n = inet_pton($ip); + $bits = 15; // 16 x 8 bit = 128bit + $ipv6long = ''; + while ($bits >= 0) { + $bin = sprintf("%08b", (ord($ip_n[$bits]))); + $ipv6long = $bin . $ipv6long; + $bits--; + } + return gmp_strval(gmp_init($ipv6long, 2), 10); + } + + private static function inet6_expand(string $addr) + { + // Check if there are segments missing, insert if necessary + if (strpos($addr, '::') !== false) { + $part = explode('::', $addr); + $part[0] = explode(':', $part[0]); + $part[1] = explode(':', $part[1]); + $missing = array(); + for ($i = 0; $i < (8 - (count($part[0]) + count($part[1]))); $i++) + array_push($missing, '0000'); + $missing = array_merge($part[0], $missing); + $part = array_merge($missing, $part[1]); + } else { + $part = explode(":", $addr); + } + // Pad each segment until it has 4 digits + foreach ($part as &$p) { + while (strlen($p) < 4) $p = '0' . $p; + } + unset($p); + // Join segments + $result = implode(':', $part); + // Quick check to make sure the length is as expected + if (strlen($result) == 39) { + return $result; + } else { + return false; + } + } + + private static function inet6_prefix_to_mask($prefix) + { + /* Make sure the prefix is a number between 1 and 127 (inclusive) */ + $prefix = intval($prefix); + if ($prefix < 0 || $prefix > 128) return false; + $mask = '0b'; + for ($i = 0; $i < $prefix; $i++) $mask .= '1'; + for ($i = strlen($mask) - 2; $i < 128; $i++) $mask .= '0'; + $mask = gmp_strval(gmp_init($mask), 16); + $result = ''; + for ($i = 0; $i < 8; $i++) { + $result .= substr($mask, $i * 4, 4); + if ($i != 7) $result .= ':'; + } // for + return inet_ntop(inet_pton($result)); + } +} diff --git a/lib/Froxlor/UI/Callbacks/Domain.php b/lib/Froxlor/UI/Callbacks/Domain.php index 95e876c9..42c9e4c9 100644 --- a/lib/Froxlor/UI/Callbacks/Domain.php +++ b/lib/Froxlor/UI/Callbacks/Domain.php @@ -61,6 +61,11 @@ class Domain return UI::getLng('domains.aliasdomain') . ' ' . $attributes['fields']['aliasdomain']; } + public static function domainExternalLink(array $attributes) + { + return '' . $attributes['data'] . ''; + } + public static function canEdit(array $attributes): bool { return (bool)$attributes['fields']['caneditdomain']; diff --git a/lib/Froxlor/Validate/Validate.php b/lib/Froxlor/Validate/Validate.php index e8632d06..2b7367a4 100644 --- a/lib/Froxlor/Validate/Validate.php +++ b/lib/Froxlor/Validate/Validate.php @@ -1,6 +1,9 @@ [ 'label' => $lng['domains']['domainname'], 'field' => 'domain_ace', + 'callback' => [Domain::class, 'domainExternalLink'], ], 'd.documentroot' => [ 'label' => $lng['panel']['path'], diff --git a/tests/Froxlor/IPToolsTest.php b/tests/Froxlor/IPToolsTest.php new file mode 100644 index 00000000..1ddec3be --- /dev/null +++ b/tests/Froxlor/IPToolsTest.php @@ -0,0 +1,40 @@ +assertFalse($result); + $result = IPTools::is_ipv6('1.1.1.1'); + $this->assertFalse($result); + $result = IPTools::is_ipv6('::ffff:10.20.30.40'); + $this->assertEquals('::ffff:10.20.30.40', $result); + $result = IPTools::is_ipv6('2620:0:2d0:200::7/32'); + $this->assertFalse($result); + $result = IPTools::is_ipv6('2620:0:2d0:200::7'); + $this->assertEquals('2620:0:2d0:200::7', $result); + } + + public function testValidateIPinRange() + { + $result = IPTools::ip_in_range([0=>'82.149.225.46',1=>24], '123.213.132.1'); + $this->assertFalse($result); + $result = IPTools::ip_in_range([0=>'82.149.225.46',1=>24], '2620:0:2d0:200::7'); + $this->assertFalse($result); + $result = IPTools::ip_in_range([0=>'82.149.225.46',1=>24], '82.149.225.152'); + $this->assertTrue($result); + $result = IPTools::ip_in_range([0=>'2620:0:2d0:200::1',1=>116], '2620:0:2d0:200::fff1'); + $this->assertFalse($result); + $result = IPTools::ip_in_range([0=>'2620:0:2d0:200::1',1=>64], '2620:0:2d0:200::fff1'); + $this->assertTrue($result); + } +} diff --git a/tests/Froxlor/ValidateTest.php b/tests/Froxlor/ValidateTest.php index d973154a..6cb252a1 100644 --- a/tests/Froxlor/ValidateTest.php +++ b/tests/Froxlor/ValidateTest.php @@ -109,20 +109,6 @@ class ValidateTest extends TestCase $this->assertEquals("8.8.8.8/128.0.0.0", $result); } - public function testValidateIPv6() - { - $result = Validate::is_ipv6('1.1.1.1/4'); - $this->assertFalse($result); - $result = Validate::is_ipv6('1.1.1.1'); - $this->assertFalse($result); - $result = Validate::is_ipv6('::ffff:10.20.30.40'); - $this->assertEquals('::ffff:10.20.30.40', $result); - $result = Validate::is_ipv6('2620:0:2d0:200::7/32'); - $this->assertFalse($result); - $result = Validate::is_ipv6('2620:0:2d0:200::7'); - $this->assertEquals('2620:0:2d0:200::7', $result); - } - public function testValidateIpLocalhostAllowedWrongIp() { $this->expectException("Exception");