public
nobgit
read
NobMail
Based on mailcow: dockerized
Languages
Repository composition by tracked source files.
PHP
49%
JavaScript
35%
HTML
9%
CSS
4%
Shell
2%
Python
1%
Lua
0%
Perl
0%
Ruby
0%
SCSS
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/orgs/nobgit/nobmail.git
ssh://[email protected]:2222/orgs/nobgit/nobmail.git
Commit
[Web] Sync jobs: Use STARTTLS instead of TLS; Feature: Allow to edit fail2ban-like regex filters in UI
877b9b70
data/web/admin.php | 35 ++++++++-
data/web/css/site/admin.css | 4 +
data/web/edit.php | 6 +-
data/web/inc/functions.fail2ban.inc.php | 128 ++++++++++++++++++++++++++------
data/web/js/site/admin.js | 19 ++++-
data/web/lang/lang.de.json | 5 +-
data/web/lang/lang.en.json | 3 +
data/web/modals/mailbox.php | 6 +-
data/web/modals/user.php | 6 +-
9 files changed, 176 insertions(+), 36 deletions(-)
Diff
diff --git a/data/web/admin.php b/data/web/admin.php
index fbf09035..bf482155 100644
--- a/data/web/admin.php
+++ b/data/web/admin.php
@@ -698,6 +698,7 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
<input type="number" class="form-control" name="netban_ipv6" value="<?=$f2b_data['netban_ipv6'];?>" required>
</div>
</div>
+ <hr>
<p class="help-block"><?=$lang['admin']['f2b_list_info'];?></p>
<div class="form-group">
<label for="whitelist"><?=$lang['admin']['f2b_whitelist'];?>:</label>
@@ -708,11 +709,41 @@ if (!isset($_SESSION['gal']) && $license_cache = $redis->Get('LICENSE_STATUS_CAC
<textarea class="form-control" name="blacklist" rows="5"><?=$f2b_data['blacklist'];?></textarea>
</div>
<div class="btn-group">
- <button class="btn btn-success" data-action="edit_selected" data-item="self" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
- <a href="#" role="button" class="btn btn-default" data-toggle="modal" data-container="netfilter-mailcow" data-target="#RestartContainer"><span class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_netfilter']; ?></a>
+ <button class="btn btn-sm btn-success" data-action="edit_selected" data-item="self" data-id="f2b" data-api-url='edit/fail2ban' data-api-attr='{}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+ <a href="#" role="button" class="btn btn-sm btn-default" data-toggle="modal" data-container="netfilter-mailcow" data-target="#RestartContainer"><span class="glyphicon glyphicon-refresh"></span> <?= $lang['header']['restart_netfilter']; ?></a>
</div>
</form>
<hr>
+ <h4><?=$lang['admin']['f2b_filter'];?></h4>
+ <p class="help-block"><?=$lang['admin']['f2b_regex_info'];?></p>
+ <form class="form-inline" data-id="f2b_regex" role="form" method="post">
+ <table class="table table-condensed" id="f2b_regex_table">
+ <tr>
+ <th width="50px">ID</th>
+ <th>RegExp</th>
+ <th width="100px"> </th>
+ </tr>
+ <?php
+ if (!empty($f2b_data['regex'])) {
+ foreach ($f2b_data['regex'] as $regex_id => $regex_val) {
+ ?>
+ <tr>
+ <td><input disabled class="input-sm form-control" style="text-align:center" data-id="f2b_regex" type="text" name="app" required value="<?=$regex_id;?>"></td>
+ <td><input class="input-sm form-control regex-input" data-id="f2b_regex" type="text" name="regex" required value="<?=htmlspecialchars($regex_val);?>"></td>
+ <td><a href="#" role="button" class="btn btn-xs btn-default" type="button"><?=$lang['admin']['remove_row'];?></a></td>
+ </tr>
+ <?php
+ }
+ }
+ ?>
+ </table>
+ <p><div class="btn-group">
+ <button class="btn btn-sm btn-success" data-action="edit_selected" data-item="admin" data-id="f2b_regex" data-reload="no" data-api-url='edit/fail2ban' data-api-attr='{"action":"edit-regex"}' href="#"><span class="glyphicon glyphicon-check"></span> <?=$lang['admin']['save'];?></button>
+ <button class="btn btn-sm btn-default admin-ays-dialog" data-action="edit_selected" data-item="self" data-id="f2b-quick" data-api-url='edit/fail2ban' data-api-attr='{"action":"reset-regex"}' href="#"><?=$lang['admin']['reset_default'];?></button>
+ <button class="btn btn-sm btn-default" type="button" id="add_f2b_regex_row"><span class="glyphicon glyphicon-plus"></span> <?=$lang['admin']['add_row'];?></button>
+ </div></p>
+ </form>
+ <hr>
<p class="help-block"><?=$lang['admin']['ban_list_info'];?></p>
<?php
if (empty($f2b_data['active_bans']) && empty($f2b_data['perm_bans'])):
diff --git a/data/web/css/site/admin.css b/data/web/css/site/admin.css
index 81fe98c3..e06a6d03 100644
--- a/data/web/css/site/admin.css
+++ b/data/web/css/site/admin.css
@@ -72,4 +72,8 @@ table tbody tr td input[type="checkbox"] {
}
#quarantine_template {
margin:20px;
+}
+.regex-input {
+ font-family: Consolas,monaco,monospace;
+ font-size: 14px;
}
\ No newline at end of file
diff --git a/data/web/edit.php b/data/web/edit.php
index 520b18db..63728b37 100644
--- a/data/web/edit.php
+++ b/data/web/edit.php
@@ -1234,9 +1234,9 @@ if (isset($_SESSION['mailcow_cc_role'])) {
<label class="control-label col-sm-2" for="enc1"><?=$lang['edit']['encryption'];?></label>
<div class="col-sm-10">
<select id="enc1" name="enc1">
- <option <?=($result['enc1'] == "TLS") ? "selected" : null;?>>TLS</option>
- <option <?=($result['enc1'] == "SSL") ? "selected" : null;?>>SSL</option>
- <option <?=($result['enc1'] == "PLAIN") ? "selected" : null;?>>PLAIN</option>
+ <option value="SSL" <?=($result['enc1'] == "SSL") ? "selected" : null;?>>SSL</option>
+ <option value="TLS" <?=($result['enc1'] == "TLS") ? "selected" : null;?>>STARTTLS</option>
+ <option value="PLAIN" <?=($result['enc1'] == "PLAIN") ? "selected" : null;?>>PLAIN</option>
</select>
</div>
</div>
diff --git a/data/web/inc/functions.fail2ban.inc.php b/data/web/inc/functions.fail2ban.inc.php
index 066c8d59..72d60062 100644
--- a/data/web/inc/functions.fail2ban.inc.php
+++ b/data/web/inc/functions.fail2ban.inc.php
@@ -10,6 +10,7 @@ function fail2ban($_action, $_data = null) {
}
try {
$f2b_options = json_decode($redis->Get('F2B_OPTIONS'), true);
+ $f2b_options['regex'] = json_decode($redis->Get('F2B_REGEX'), true);
$wl = $redis->hGetAll('F2B_WHITELIST');
if (is_array($wl)) {
foreach ($wl as $key => $value) {
@@ -87,20 +88,101 @@ function fail2ban($_action, $_data = null) {
);
return false;
}
- if (isset($_data['action']) && !empty($_data['network'])) {
- $networks = (array) $_data['network'];
- foreach ($networks as $network) {
+ // Start to read actions, if any
+ if (isset($_data['action'])) {
+ // Reset regex filters
+ if ($_data['action'] == "reset-regex") {
try {
+ $redis->Del('F2B_REGEX');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ return false;
+ }
+ // Rules will also be recreated on log events, but rules may seem empty for a second in the UI
+ docker('post', 'netfilter-mailcow', 'restart');
+ $fail_count = 0;
+ $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
+ while (empty($regex_result) && $fail_count < 10) {
+ $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
+ $fail_count++;
+ sleep(1);
+ }
+ if ($fail_count >= 10) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('reset_f2b_regex')
+ );
+ return false;
+ }
+ }
+ elseif ($_data['action'] == "edit-regex") {
+ if (!empty($_data['regex'])) {
+ $rule_id = 1;
+ $regex_array = array();
+ foreach($_data['regex'] as $regex) {
+ $regex_array[$rule_id] = $regex;
+ $rule_id++;
+ }
+ $redis->Set('F2B_REGEX', json_encode($regex_array, JSON_UNESCAPED_SLASHES));
+ }
+ else {
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => print_r($_data, true)
+ );
+ return false;
+ }
+ $_SESSION['return'][] = array(
+ 'type' => 'success',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('object_modified', htmlspecialchars($network))
+ );
+ return true;
+ }
+
+ // Start actions in dependency of network
+ if (!empty($_data['network'])) {
+ $networks = (array)$_data['network'];
+ foreach ($networks as $network) {
+ // Unban network
if ($_data['action'] == "unban") {
if (valid_network($network)) {
- $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
+ try {
+ $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
}
}
+ // Whitelist network
elseif ($_data['action'] == "whitelist") {
if (valid_network($network)) {
- $redis->hSet('F2B_WHITELIST', $network, 1);
- $redis->hDel('F2B_BLACKLIST', $network, 1);
- $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
+ try {
+ $redis->hSet('F2B_WHITELIST', $network, 1);
+ $redis->hDel('F2B_BLACKLIST', $network, 1);
+ $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
}
else {
$_SESSION['return'][] = array(
@@ -111,6 +193,7 @@ function fail2ban($_action, $_data = null) {
continue;
}
}
+ // Blacklist network
elseif ($_data['action'] == "blacklist") {
if (valid_network($network) && !in_array($network, array(
'0.0.0.0',
@@ -119,9 +202,19 @@ function fail2ban($_action, $_data = null) {
getenv('IPV4_NETWORK') . '0',
getenv('IPV6_NETWORK')
))) {
- $redis->hSet('F2B_BLACKLIST', $network, 1);
- $redis->hDel('F2B_WHITELIST', $network, 1);
- //$response = docker('post', 'netfilter-mailcow', 'restart');
+ try {
+ $redis->hSet('F2B_BLACKLIST', $network, 1);
+ $redis->hDel('F2B_WHITELIST', $network, 1);
+ //$response = docker('post', 'netfilter-mailcow', 'restart');
+ }
+ catch (RedisException $e) {
+ $_SESSION['return'][] = array(
+ 'type' => 'danger',
+ 'log' => array(__FUNCTION__, $_action, $_data_log),
+ 'msg' => array('redis_error', $e)
+ );
+ continue;
+ }
}
else {
$_SESSION['return'][] = array(
@@ -132,23 +225,16 @@ function fail2ban($_action, $_data = null) {
continue;
}
}
- }
- catch (RedisException $e) {
$_SESSION['return'][] = array(
- 'type' => 'danger',
+ 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('redis_error', $e)
+ 'msg' => array('object_modified', htmlspecialchars($network))
);
- continue;
}
- $_SESSION['return'][] = array(
- 'type' => 'success',
- 'log' => array(__FUNCTION__, $_action, $_data_log),
- 'msg' => array('object_modified', htmlspecialchars($network))
- );
+ return true;
}
- return true;
}
+ // Start default edit without specific action
$is_now = fail2ban('get');
if (!empty($is_now)) {
$ban_time = intval((isset($_data['ban_time'])) ? $_data['ban_time'] : $is_now['ban_time']);
diff --git a/data/web/js/site/admin.js b/data/web/js/site/admin.js
index 027ff96f..cb2c91ea 100644
--- a/data/web/js/site/admin.js
+++ b/data/web/js/site/admin.js
@@ -460,11 +460,17 @@ jQuery(function($){
}
})
// App links
- function add_table_row(table_id) {
+ function add_table_row(table_id, type) {
var row = $('<tr />');
+ if (type == "app_link") {
cols = '<td><input class="input-sm form-control" data-id="app_links" type="text" name="app" required></td>';
cols += '<td><input class="input-sm form-control" data-id="app_links" type="text" name="href" required></td>';
- cols += '<td><a href="#" role="button" class="btn btn-xs btn-default" type="button">Remove row</a></td>';
+ cols += '<td><a href="#" role="button" class="btn btn-xs btn-default" type="button">">' + lang.remove_row + '</a></td>';
+ } else if (type == "f2b_regex") {
+ cols = '<td><input style="text-align:center" class="input-sm form-control" data-id="f2b_regex" type="text" value="+" disabled></td>';
+ cols += '<td><input class="input-sm form-control regex-input" data-id="f2b_regex" type="text" name="regex" required></td>';
+ cols += '<td><a href="#" role="button" class="btn btn-xs btn-default" type="button">' + lang.remove_row + '</a></td>';
+ }
row.append(cols);
table_id.append(row);
}
@@ -472,8 +478,15 @@ jQuery(function($){
e.preventDefault();
$(this).parents('tr').remove();
});
+ $('#f2b_regex_table').on('click', 'tr a', function (e) {
+ e.preventDefault();
+ $(this).parents('tr').remove();
+ });
$('#add_app_link_row').click(function() {
- add_table_row($('#app_link_table'));
+ add_table_row($('#app_link_table'), "app_link");
+ });
+ $('#add_f2b_regex_row').click(function() {
+ add_table_row($('#f2b_regex_table'), "f2b_regex");
});
});
diff --git a/data/web/lang/lang.de.json b/data/web/lang/lang.de.json
index a63b3350..876dfc82 100644
--- a/data/web/lang/lang.de.json
+++ b/data/web/lang/lang.de.json
@@ -74,7 +74,7 @@
"password": "Passwort",
"password_repeat": "Passwort wiederholen",
"port": "Port",
- "post_domain_add": "Der SOGo-Container, \"sogo-mailcow\" muss nach dem Hinzufügen einer Domain neugestartet werden!<br><br>Im Anschluss sollte die DNS-Konfiguration der Domain überprüft- und \"acme-mailcow\" gegebenenfalls neugestartet werden, um Änderungen am Zertifikat zu übernehmen (autoconfig.<domain>, autodiscover.<domain>).<br>Dieser Schritt ist optional und wird alle 24 Stunden automatisch ausgeführt.",
+ "post_domain_add": "Der SOGo-Container - \"sogo-mailcow\" - muss nach dem Hinzufügen einer Domain neugestartet werden!<br><br>Im Anschluss sollte die DNS-Konfiguration der Domain überprüft- und \"acme-mailcow\" gegebenenfalls neugestartet werden, um Änderungen am Zertifikat zu übernehmen (autoconfig.<domain>, autodiscover.<domain>).<br>Dieser Schritt ist optional und wird alle 24 Stunden automatisch ausgeführt.",
"private_comment": "Privater Kommentar",
"public_comment": "Öffentlicher Kommentar",
"quota_mb": "Speicherplatz (MiB)",
@@ -165,11 +165,13 @@
"excludes": "Diese Empfänger ausschließen",
"f2b_ban_time": "Bannzeit in Sekunden",
"f2b_blacklist": "Blacklist für Netzwerke und Hosts",
+ "f2b_filter": "Regex Filter",
"f2b_list_info": "Ein Host oder Netzwerk auf der Blacklist wird immer eine Whitelist-Einheit überwiegen. <b>Die Aktualisierung der Liste dauert einige Sekunden.</b>",
"f2b_max_attempts": "Max. Versuche",
"f2b_netban_ipv4": "Netzbereich für IPv4-Bans (8-32)",
"f2b_netban_ipv6": "Netzbereich für IPv6-Bans (8-128)",
"f2b_parameters": "Fail2ban-Parameter",
+ "f2b_regex_info": "Berücksichtigte Logs: SOGo, Postfix, Dovecot, PHP-FPM.",
"f2b_retry_window": "Wiederholungen im Zeitraum von (s)",
"f2b_whitelist": "Whitelist für Netzwerke und Hosts",
"filter_table": "Tabelle filtern",
@@ -396,6 +398,7 @@
"redis_error": "Redis Fehler: %s",
"relayhost_invalid": "Mapeintrag %s ist ungültig",
"release_send_failed": "Die Nachricht konnte nicht versendet werden: %s",
+ "reset_f2b_regex": "Regex Filter konnten nicht in vorgegebener Zeit zurückgesetzt werden, bitte erneut versuchen oder die Webseite neu laden.",
"resource_invalid": "Ressourcenname %s ist ungültig",
"rl_timeframe": "Ratelimit-Zeitraum ist inkorrekt",
"rspamd_ui_pw_length": "Rspamd UI-Passwort muss mindestens 6 Zeichen lang sein",
diff --git a/data/web/lang/lang.en.json b/data/web/lang/lang.en.json
index f416d025..be87b83b 100644
--- a/data/web/lang/lang.en.json
+++ b/data/web/lang/lang.en.json
@@ -164,11 +164,13 @@
"excludes": "Excludes these recipients",
"f2b_ban_time": "Ban time (s)",
"f2b_blacklist": "Blacklisted networks/hosts",
+ "f2b_filter": "Regex filters",
"f2b_list_info": "A blacklisted host or network will always outweigh a whitelist entity. <b>List updates will take a few seconds to be applied.</b>",
"f2b_max_attempts": "Max. attempts",
"f2b_netban_ipv4": "IPv4 subnet size to apply ban on (8-32)",
"f2b_netban_ipv6": "IPv6 subnet size to apply ban on (8-128)",
"f2b_parameters": "Fail2ban parameters",
+ "f2b_regex_info": "Logs taken into consideration: SOGo, Postfix, Dovecot, PHP-FPM.",
"f2b_retry_window": "Retry window (s) for max. attempts",
"f2b_whitelist": "Whitelisted networks/hosts",
"filter_table": "Filter table",
@@ -395,6 +397,7 @@
"redis_error": "Redis error: %s",
"relayhost_invalid": "Map entry %s is invalid",
"release_send_failed": "Message could not be released: %s",
+ "reset_f2b_regex": "Regex filter could not be reset in time, please try again or wait a few more seconds and reload the website.",
"resource_invalid": "Resource name %s is invalid",
"rl_timeframe": "Rate limit time frame is incorrect",
"rspamd_ui_pw_length": "Rspamd UI password should be at least 6 chars long",
diff --git a/data/web/modals/mailbox.php b/data/web/modals/mailbox.php
index 063a1066..d0a82f47 100644
--- a/data/web/modals/mailbox.php
+++ b/data/web/modals/mailbox.php
@@ -448,9 +448,9 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
<label class="control-label col-sm-2" for="enc1"><?=$lang['add']['enc_method'];?></label>
<div class="col-sm-10">
<select name="enc1" title="<?=$lang['add']['select'];?>" required>
- <option selected>TLS</option>
- <option>SSL</option>
- <option>PLAIN</option>
+ <option value="SSL" selected>SSL</option>
+ <option value="TLS">STARTTLS</option>
+ <option value="PLAIN">PLAIN</option>
</select>
</div>
</div>
diff --git a/data/web/modals/user.php b/data/web/modals/user.php
index e4c18611..216717e2 100644
--- a/data/web/modals/user.php
+++ b/data/web/modals/user.php
@@ -44,9 +44,9 @@ if (!isset($_SESSION['mailcow_cc_role'])) {
<label class="control-label col-sm-2" for="enc1"><?=$lang['add']['enc_method'];?></label>
<div class="col-sm-10">
<select name="enc1" title="<?=$lang['add']['select'];?>" required>
- <option selected>TLS</option>
- <option>SSL</option>
- <option>PLAIN</option>
+ <option value="SSL" selected>SSL</option>
+ <option value="TLS">STARTTLS</option>
+ <option value="PLAIN">PLAIN</option>
</select>
</div>
</div>