add new IPTools class; add new callback to show link to domain in domain-overview; validate possible allowed_ip-ranges in FroxlorRPC; fix possible duplicate ips for mysql-access-host in installation
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
@@ -45,6 +45,7 @@
|
||||
"ext-json": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-gmp": "*",
|
||||
"phpmailer/phpmailer": "~6.0",
|
||||
"monolog/monolog": "^1.24",
|
||||
"robthree/twofactorauth": "^1.6",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\Settings;
|
||||
use Froxlor\Validate\Validate;
|
||||
use Froxlor\System\IPTools;
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
@@ -892,7 +892,7 @@ if (\Froxlor\Froxlor::isDatabaseVersion('202107210')) {
|
||||
Database::pexecute($result_stmt);
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_IPSANDPORTS . "` SET `ip` = :ip WHERE `id` = :id");
|
||||
while ($iprow = $result_stmt->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,
|
||||
|
||||
@@ -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
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
|
||||
165
lib/Froxlor/System/IPTools.php
Normal file
165
lib/Froxlor/System/IPTools.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace Froxlor\System;
|
||||
|
||||
class IPTools
|
||||
{
|
||||
|
||||
/**
|
||||
* Converts CIDR to a netmask address
|
||||
*
|
||||
* @thx to https://stackoverflow.com/a/5711080/3020926
|
||||
* @param string $cidr
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function cidr2NetmaskAddr($cidr)
|
||||
{
|
||||
$ta = substr($cidr, strpos($cidr, '/') + 1) * 1;
|
||||
$netmask = str_split(str_pad(str_pad('', $ta, '1'), 32, '0'), 8);
|
||||
|
||||
foreach ($netmask as &$element) {
|
||||
$element = bindec($element);
|
||||
}
|
||||
|
||||
return implode('.', $netmask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an $address (IP) is IPv6
|
||||
*
|
||||
* @param string $address
|
||||
*
|
||||
* @return string|bool ip address on success, false on failure
|
||||
*/
|
||||
public static function is_ipv6($address)
|
||||
{
|
||||
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the given $ip is in range of given ip/cidr range
|
||||
*
|
||||
* @param array $ip_cidr 0 => 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));
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,11 @@ class Domain
|
||||
return UI::getLng('domains.aliasdomain') . ' ' . $attributes['fields']['aliasdomain'];
|
||||
}
|
||||
|
||||
public static function domainExternalLink(array $attributes)
|
||||
{
|
||||
return '<a href="http://' . $attributes['data'] . '" target="_blank">' . $attributes['data'] . '</a>';
|
||||
}
|
||||
|
||||
public static function canEdit(array $attributes): bool
|
||||
{
|
||||
return (bool)$attributes['fields']['caneditdomain'];
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Froxlor\Validate;
|
||||
|
||||
use Froxlor\System\IPTools;
|
||||
|
||||
class Validate
|
||||
{
|
||||
|
||||
@@ -31,7 +34,7 @@ class Validate
|
||||
*/
|
||||
public static function validate($str, $fieldname, $pattern = '', $lng = '', $emptydefault = array(), $throw_exception = false)
|
||||
{
|
||||
if (! is_array($emptydefault)) {
|
||||
if (!is_array($emptydefault)) {
|
||||
$emptydefault_array = array(
|
||||
$emptydefault
|
||||
);
|
||||
@@ -41,7 +44,7 @@ class Validate
|
||||
}
|
||||
|
||||
// Check if the $str is one of the values which represent the default for an 'empty' value
|
||||
if (is_array($emptydefault) && ! empty($emptydefault) && in_array($str, $emptydefault)) {
|
||||
if (is_array($emptydefault) && !empty($emptydefault) && in_array($str, $emptydefault)) {
|
||||
return $str;
|
||||
}
|
||||
|
||||
@@ -49,7 +52,7 @@ class Validate
|
||||
|
||||
$pattern = '/^[^\r\n\t\f\0]*$/D';
|
||||
|
||||
if (! preg_match($pattern, $str)) {
|
||||
if (!preg_match($pattern, $str)) {
|
||||
// Allows letters a-z, digits, space (\\040), hyphen (\\-), underscore (\\_) and backslash (\\\\),
|
||||
// everything else is removed from the string.
|
||||
$allowed = "/[^a-z0-9\\040\\.\\-\\_\\\\]/i";
|
||||
@@ -70,38 +73,6 @@ class Validate
|
||||
\Froxlor\UI\Response::standard_error($lng, $fieldname, $throw_exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts CIDR to a netmask address
|
||||
*
|
||||
* @thx to https://stackoverflow.com/a/5711080/3020926
|
||||
* @param string $cidr
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function cidr2NetmaskAddr($cidr)
|
||||
{
|
||||
$ta = substr($cidr, strpos($cidr, '/') + 1) * 1;
|
||||
$netmask = str_split(str_pad(str_pad('', $ta, '1'), 32, '0'), 8);
|
||||
|
||||
foreach ($netmask as &$element) {
|
||||
$element = bindec($element);
|
||||
}
|
||||
|
||||
return implode('.', $netmask);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an $address (IP) is IPv6
|
||||
*
|
||||
* @param string $address
|
||||
*
|
||||
* @return string|bool ip address on success, false on failure
|
||||
*/
|
||||
public static function is_ipv6($address)
|
||||
{
|
||||
return filter_var($address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether it is a valid ip
|
||||
*
|
||||
@@ -131,10 +102,14 @@ class Validate
|
||||
$org_ip = $ip;
|
||||
$ip_cidr = explode("/", $ip);
|
||||
if (count($ip_cidr) === 2) {
|
||||
if (strlen($ip_cidr[1]) <= 2 && in_array((int) $ip_cidr[1], array_values(range(1, 32)), true) === false) {
|
||||
$cidr_range_max = 32;
|
||||
if (IPTools::is_ipv6($ip_cidr[0])) {
|
||||
$cidr_range_max = 128;
|
||||
}
|
||||
if (strlen($ip_cidr[1]) <= 3 && in_array((int) $ip_cidr[1], array_values(range(1, $cidr_range_max)), true) === false) {
|
||||
\Froxlor\UI\Response::standard_error($lng, $ip, $throw_exception);
|
||||
}
|
||||
if ($cidr_as_netmask && self::is_ipv6($ip_cidr[0])) {
|
||||
if ($cidr_as_netmask && IPTools::is_ipv6($ip_cidr[0])) {
|
||||
// MySQL does not handle CIDR of IPv6 addresses, return error
|
||||
if ($return_bool) {
|
||||
return false;
|
||||
@@ -143,8 +118,8 @@ class Validate
|
||||
}
|
||||
}
|
||||
$ip = $ip_cidr[0];
|
||||
if ($cidr_as_netmask && strlen($ip_cidr[1]) <= 2) {
|
||||
$ip_cidr[1] = self::cidr2NetmaskAddr($org_ip);
|
||||
if ($cidr_as_netmask && strlen($ip_cidr[1]) <= 3) {
|
||||
$ip_cidr[1] = IPTools::cidr2NetmaskAddr($org_ip);
|
||||
}
|
||||
$cidr = "/" . $ip_cidr[1];
|
||||
} else {
|
||||
@@ -279,10 +254,10 @@ class Validate
|
||||
*/
|
||||
public static function validateUsername($username, $unix_names = 1, $mysql_max = '')
|
||||
{
|
||||
if (empty($mysql_max) || ! is_numeric($mysql_max) || $mysql_max <= 0) {
|
||||
if (empty($mysql_max) || !is_numeric($mysql_max) || $mysql_max <= 0) {
|
||||
$mysql_max = \Froxlor\Database\Database::getSqlUsernameLength() - 1;
|
||||
} else {
|
||||
$mysql_max --;
|
||||
$mysql_max--;
|
||||
}
|
||||
if ($unix_names == 0) {
|
||||
if (strpos($username, '--') === false) {
|
||||
@@ -302,7 +277,7 @@ class Validate
|
||||
*/
|
||||
public static function validateSqlInterval($interval = null)
|
||||
{
|
||||
if (! empty($interval) && strstr($interval, ' ') !== false) {
|
||||
if (!empty($interval) && strstr($interval, ' ') !== false) {
|
||||
/*
|
||||
* [0] = ([0-9]+)
|
||||
* [1] = valid SQL-Interval expression
|
||||
|
||||
@@ -31,6 +31,7 @@ return [
|
||||
'd.domain_ace' => [
|
||||
'label' => $lng['domains']['domainname'],
|
||||
'field' => 'domain_ace',
|
||||
'callback' => [Domain::class, 'domainExternalLink'],
|
||||
],
|
||||
'd.documentroot' => [
|
||||
'label' => $lng['panel']['path'],
|
||||
|
||||
40
tests/Froxlor/IPToolsTest.php
Normal file
40
tests/Froxlor/IPToolsTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use Froxlor\System\IPTools;
|
||||
|
||||
/**
|
||||
*
|
||||
* @covers \Froxlor\System\IPTools
|
||||
*/
|
||||
class IPToolsTest extends TestCase
|
||||
{
|
||||
public function testValidateIPv6()
|
||||
{
|
||||
$result = IPTools::is_ipv6('1.1.1.1/4');
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user