diff --git a/data/web/admin.php b/data/web/admin.php index 8c5c606f..96e4f189 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -113,77 +113,71 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC - - API + + API +
-
-
- -
-
-
-
- -
- -
-
-
-
- -
-
-
- -
-
-
-
-
-
- -
-
-
-
-

-
- - +
+
+
+

⇇ Read-Only Access

+
+
+ +
+ +
+
-
- -
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+

+
+ + +
+
+
+ +
+
-

- - ⇄ Read-Write Access -

+

⇄ Read-Write Access

-
- +
@@ -210,17 +204,16 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC

- - + +
-
+
-
diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php index 6ec23d34..d607cc2a 100644 --- a/data/web/inc/functions.fail2ban.inc.php +++ b/data/web/inc/functions.fail2ban.inc.php @@ -1,19 +1,4 @@ = 0 && $cidr[1] <= 32))) { - return true; - } - elseif (filter_var($cidr[0], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && (!isset($cidr[1]) || ($cidr[1] >= 0 && $cidr[1] <= 128))) { - return true; - } - return false; -} - -function valid_hostname($hostname) { - return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); -} - function fail2ban($_action, $_data = null) { global $redis; global $lang; @@ -196,6 +181,14 @@ function fail2ban($_action, $_data = null) { if (valid_network($wl_item) || valid_hostname($wl_item)) { $redis->hSet('F2B_WHITELIST', $wl_item, 1); } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('network_host_invalid', $wl_item) + ); + continue; + } } } } @@ -206,6 +199,14 @@ function fail2ban($_action, $_data = null) { if (valid_network($bl_item) || valid_hostname($bl_item)) { $redis->hSet('F2B_BLACKLIST', $bl_item, 1); } + else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_data_log), + 'msg' => array('network_host_invalid', $bl_item) + ); + continue; + } } } } diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index 9f74f89f..6cc052fb 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -10,6 +10,76 @@ function isset_has_content($var) { return false; } } +// Validates ips and cidrs +function valid_network($network) { + if (filter_var($network, FILTER_VALIDATE_IP)) { + return true; + } + $parts = explode('/', $network); + if (count($parts) != 2) { + return false; + } + $ip = $parts[0]; + $netmask = $parts[1]; + if (!preg_match("/^\d+$/", $netmask)){ + return false; + } + $netmask = intval($parts[1]); + if ($netmask < 0) { + return false; + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + return $netmask <= 32; + } + if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + return $netmask <= 128; + } + return false; +} +function valid_hostname($hostname) { + return filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); +} +// Thanks to https://stackoverflow.com/a/49373789 +// Validates exact ip matches and ip-in-cidr, ipv4 and ipv6 +function ip_acl($ip, $networks) { + foreach($networks as $network) { + if (filter_var($network, FILTER_VALIDATE_IP)) { + if ($ip == $network) { + return true; + } + else { + continue; + } + } + $ipb = inet_pton($ip); + $iplen = strlen($ipb); + if (strlen($ipb) < 4) { + continue; + } + $ar = explode('/', $network); + $ip1 = $ar[0]; + $ip1b = inet_pton($ip1); + $ip1len = strlen($ip1b); + if ($ip1len != $iplen) { + continue; + } + if (count($ar)>1) { + $bits=(int)($ar[1]); + } + else { + $bits = $iplen * 8; + } + for ($c=0; $bits>0; $c++) { + $bytemask = ($bits < 8) ? 0xff ^ ((1 << (8-$bits))-1) : 0xff; + if (((ord($ipb[$c]) ^ ord($ip1b[$c])) & $bytemask) != 0) { + continue 2; + } + $bits-=8; + } + return true; + } + return false; +} function hash_password($password) { $salt_str = bin2hex(openssl_random_pseudo_bytes(8)); return "{SSHA256}".base64_encode(hash('sha256', $password . $salt_str, true) . $salt_str); @@ -1160,178 +1230,106 @@ function admin_api($access, $action, $data = null) { ); return false; } - switch ($access) { - case "rw": - switch ($action) { - case "edit": - $active = (isset($data['active'])) ? 1 : 0; - $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0; - $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from'])); - foreach ($allow_from as $key => $val) { - if (empty($val)) { - continue; - } - if (!filter_var($val, FILTER_VALIDATE_IP)) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'log' => array(__FUNCTION__, $data), - 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key])) - ); - unset($allow_from[$key]); - continue; - } - } - $allow_from = implode(',', array_unique(array_filter($allow_from))); - if (empty($allow_from) && $skip_ip_check == 0) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $data), - 'msg' => 'ip_list_empty' - ); - return false; - } - $api_key = implode('-', array( - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))) - )); - $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = 'rw'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if (empty($num_results)) { - $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`) - VALUES (:api_key, :skip_ip_check, :active, :allow_from, 'rw');"); - $stmt->execute(array( - ':api_key' => $api_key, - ':skip_ip_check' => $skip_ip_check, - ':active' => $active, - ':allow_from' => $allow_from - )); - } - else { - if ($skip_ip_check == 0) { - $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check, `active` = :active, `allow_from` = :allow_from WHERE `access` = 'rw';"); - $stmt->execute(array( - ':active' => $active, - ':skip_ip_check' => $skip_ip_check, - ':allow_from' => $allow_from - )); - } - else { - $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check, `active` = :active WHERE `access` = 'rw';"); - $stmt->execute(array( - ':active' => $active, - ':skip_ip_check' => $skip_ip_check - )); - } - } - break; - case "regen_key": - $api_key = implode('-', array( - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))) - )); - $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = 'rw'"); - $stmt->execute(array( - ':api_key' => $api_key - )); - break; - case "get": - $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = 'rw'"); - $apidata = $stmt->fetch(PDO::FETCH_ASSOC); - return $apidata; - break; + if ($access !== "ro" && $access !== "rw") { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__), + 'msg' => 'invalid access type' + ); + return false; + } + if ($action == "edit") { + $active = (!empty($data['active'])) ? 1 : 0; + $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0; + $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from'])); + foreach ($allow_from as $key => $val) { + if (empty($val)) { + unset($allow_from[$key]); + continue; } - case "ro": - switch ($action) { - case "edit": - $active = (isset($data['active'])) ? 1 : 0; - $skip_ip_check = (isset($data['skip_ip_check'])) ? 1 : 0; - $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $data['allow_from'])); - foreach ($allow_from as $key => $val) { - if (empty($val)) { - continue; - } - if (!filter_var($val, FILTER_VALIDATE_IP)) { - $_SESSION['return'][] = array( - 'type' => 'warning', - 'log' => array(__FUNCTION__, $data), - 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key])) - ); - unset($allow_from[$key]); - continue; - } - } - $allow_from = implode(',', array_unique(array_filter($allow_from))); - if (empty($allow_from) && $skip_ip_check == 0) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $data), - 'msg' => 'ip_list_empty' - ); - return false; - } - $api_key = implode('-', array( - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))) - )); - $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = 'ro'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if (empty($num_results)) { - $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`) - VALUES (:api_key, :skip_ip_check, :active, :allow_from, 'ro');"); - $stmt->execute(array( - ':api_key' => $api_key, - ':skip_ip_check' => $skip_ip_check, - ':active' => $active, - ':allow_from' => $allow_from - )); - } - else { - if ($skip_ip_check == 0) { - $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check, `active` = :active, `allow_from` = :allow_from WHERE `access` = 'ro';"); - $stmt->execute(array( - ':active' => $active, - ':skip_ip_check' => $skip_ip_check, - ':allow_from' => $allow_from - )); - } - else { - $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check, `active` = :active WHERE `access` = 'ro';"); - $stmt->execute(array( - ':active' => $active, - ':skip_ip_check' => $skip_ip_check - )); - } - } - break; - case "regen_key": - $api_key = implode('-', array( - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))), - strtoupper(bin2hex(random_bytes(3))) - )); - $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = 'ro'"); - $stmt->execute(array( - ':api_key' => $api_key - )); - break; - case "get": - $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = 'ro'"); - $apidata = $stmt->fetch(PDO::FETCH_ASSOC); - return $apidata; - break; + if (valid_network($val) !== true) { + $_SESSION['return'][] = array( + 'type' => 'warning', + 'log' => array(__FUNCTION__, $data), + 'msg' => array('ip_invalid', htmlspecialchars($allow_from[$key])) + ); + unset($allow_from[$key]); + continue; } - break; + } + $allow_from = implode(',', array_unique(array_filter($allow_from))); + if (empty($allow_from) && $skip_ip_check == 0) { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $data), + 'msg' => 'ip_list_empty' + ); + return false; + } + $api_key = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + $stmt = $pdo->query("SELECT `api_key` FROM `api` WHERE `access` = '" . $access . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if (empty($num_results)) { + $stmt = $pdo->prepare("INSERT INTO `api` (`api_key`, `skip_ip_check`, `active`, `allow_from`, `access`) + VALUES (:api_key, :skip_ip_check, :active, :allow_from, :access);"); + $stmt->execute(array( + ':api_key' => $api_key, + ':skip_ip_check' => $skip_ip_check, + ':active' => $active, + ':allow_from' => $allow_from, + ':access' => $access + )); + } + else { + if ($skip_ip_check == 0) { + $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check, + `active` = :active, + `allow_from` = :allow_from + WHERE `access` = :access;"); + $stmt->execute(array( + ':active' => $active, + ':skip_ip_check' => $skip_ip_check, + ':allow_from' => $allow_from, + ':access' => $access + )); + } + else { + $stmt = $pdo->prepare("UPDATE `api` SET `skip_ip_check` = :skip_ip_check, + `active` = :active + WHERE `access` = :access;"); + $stmt->execute(array( + ':active' => $active, + ':skip_ip_check' => $skip_ip_check, + ':access' => $access + )); + } + } + } + elseif ($action == "regen_key") { + $api_key = implode('-', array( + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))), + strtoupper(bin2hex(random_bytes(3))) + )); + $stmt = $pdo->prepare("UPDATE `api` SET `api_key` = :api_key WHERE `access` = :access"); + $stmt->execute(array( + ':api_key' => $api_key, + ':access' => $access + )); + } + elseif ($action == "get") { + $stmt = $pdo->query("SELECT * FROM `api` WHERE `access` = '" . $access . "'"); + $apidata = $stmt->fetch(PDO::FETCH_ASSOC); + $apidata['allow_from'] = str_replace(',', PHP_EOL, $apidata['allow_from']); + return $apidata; } $_SESSION['return'][] = array( 'type' => 'success', diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 0601cf8e..b4d5b622 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -179,6 +179,8 @@ function get_remote_ip($anonymize = null) { } } +// Load core functions first +require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/sessions.inc.php'; // IMAP lib @@ -215,7 +217,6 @@ if(file_exists($langFile)) { $lang = array_merge_real($lang, json_decode(file_get_contents($langFile), true)); } -require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.acl.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.app_passwd.inc.php'; require_once $_SERVER['DOCUMENT_ROOT'] . '/inc/functions.mailbox.inc.php'; diff --git a/data/web/inc/sessions.inc.php b/data/web/inc/sessions.inc.php index d543458d..5c7ec710 100644 --- a/data/web/inc/sessions.inc.php +++ b/data/web/inc/sessions.inc.php @@ -53,7 +53,7 @@ if (!empty($_SERVER['HTTP_X_API_KEY'])) { $skip_ip_check = ($api_return['skip_ip_check'] == 1); $remote = get_remote_ip(false); $allow_from = array_map('trim', preg_split( "/( |,|;|\n)/", $api_return['allow_from'])); - if (in_array($remote, $allow_from) || $skip_ip_check === true) { + if ($skip_ip_check === true || ip_acl($remote, $allow_from)) { $_SESSION['mailcow_cc_username'] = 'API'; $_SESSION['mailcow_cc_role'] = 'admin'; $_SESSION['mailcow_cc_api'] = true; diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js index 43726b1d..c30dfcea 100644 --- a/data/web/js/site/admin.js +++ b/data/web/js/site/admin.js @@ -21,6 +21,7 @@ jQuery(function($){ $("#mass_exclude").change(function(){ $("#mass_include").selectpicker('deselectAll'); }); $("#mass_include").change(function(){ $("#mass_exclude").selectpicker('deselectAll'); }); $("#mass_disarm").click(function() { $("#mass_send").attr("disabled", !this.checked); }); + $(".admin-ays-dialog").click(function() { return confirm(lang.ays); }); $(".validate_rspamd_regex").click(function( event ) { event.preventDefault(); var regex_map_id = $(this).data('regex-map'); diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json index 2b859399..925c3b4c 100644 --- a/data/web/lang/lang.de.json +++ b/data/web/lang/lang.de.json @@ -122,7 +122,7 @@ "admin_details": "Administrator bearbeiten", "admin_domains": "Domain-Zuweisungen", "advanced_settings": "Erweiterte Einstellungen", - "api_allow_from": "IP-Adressen für Zugriff", + "api_allow_from": "IP-Adressen oder Netzwerke (CIDR Notation) für Zugriff auf API", "api_info": "Das API befindet sich noch in Entwicklung, die Dokumentation kann unter /api abgerufen werden.", "api_key": "API-Key", "api_skip_ip_check": "IP-Check für API nicht ausführen", @@ -131,6 +131,7 @@ "apps_name": "\"mailcow Apps\" Name", "arrival_time": "Ankunftszeit (Serverzeit)", "authed_user": "Auth. Benutzer", + "ays": "Soll der Vorgang wirklich ausgeführt werden?", "ban_list_info": "Übersicht ausgesperrter Netzwerke: Netzwerk (verbleibende Banzeit) - [Aktionen].
IPs, die zum Entsperren eingereiht werden, verlassen die Liste aktiver Bans nach wenigen Sekunden.
Rote Labels sind Indikatoren für aktive Blacklisteinträge.", "change_logo": "Logo ändern", "configuration": "Konfiguration", diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json index 41223ad7..7399f8e5 100644 --- a/data/web/lang/lang.en.json +++ b/data/web/lang/lang.en.json @@ -122,7 +122,7 @@ "admin_details": "Edit administrator details", "admin_domains": "Domain assignments", "advanced_settings": "Advanced settings", - "api_allow_from": "Allow API access from these IPs (separated by comma or new line)", + "api_allow_from": "Allow API access from these IPs/CIDR network notations", "api_info": "The API is a work in progress. The documentation can be found at /api", "api_key": "API key", "api_skip_ip_check": "Skip IP check for API", @@ -131,6 +131,7 @@ "apps_name": "\"mailcow Apps\" name", "arrival_time": "Arrival time (server time)", "authed_user": "Auth. user", + "ays": "Are you sure you want to proceed?", "ban_list_info": "See a list of banned IPs below: network (remaining ban time) - [actions].
IPs queued to be unbanned will be removed from the active ban list within a few seconds.
Red labels indicate active permanent bans by blacklisting.", "change_logo": "Change logo", "configuration": "Configuration",