Implement basic settings save and load structures and enhance UI

This commit is contained in:
Michael Reber
2025-01-27 11:09:06 +01:00
parent 75a1ef9a24
commit c2e953a024
3 changed files with 149 additions and 26 deletions

View File

@@ -3,6 +3,7 @@ package config
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log"
"os" "os"
"sync" "sync"
) )
@@ -11,6 +12,7 @@ import (
// relevant Fail2ban jail/local config options. // relevant Fail2ban jail/local config options.
type AppSettings struct { type AppSettings struct {
Language string `json:"language"` Language string `json:"language"`
Debug bool `json:"debug"`
ReloadNeeded bool `json:"reloadNeeded"` ReloadNeeded bool `json:"reloadNeeded"`
AlertCountries []string `json:"alertCountries"` AlertCountries []string `json:"alertCountries"`
@@ -56,6 +58,7 @@ func setDefaults() {
currentSettings = AppSettings{ currentSettings = AppSettings{
Language: "en", Language: "en",
Debug: false,
ReloadNeeded: false, ReloadNeeded: false,
AlertCountries: []string{"all"}, AlertCountries: []string{"all"},
@@ -71,6 +74,8 @@ func setDefaults() {
// loadSettings reads the file (if exists) into currentSettings // loadSettings reads the file (if exists) into currentSettings
func loadSettings() error { func loadSettings() error {
fmt.Println("----------------------------")
fmt.Println("loadSettings called (settings.go)") // entry point
data, err := os.ReadFile(settingsFile) data, err := os.ReadFile(settingsFile)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return err // triggers setDefaults + save return err // triggers setDefaults + save
@@ -92,14 +97,23 @@ func loadSettings() error {
// saveSettings writes currentSettings to JSON // saveSettings writes currentSettings to JSON
func saveSettings() error { func saveSettings() error {
settingsLock.RLock() fmt.Println("----------------------------")
defer settingsLock.RUnlock() fmt.Println("saveSettings called (settings.go)") // entry point
b, err := json.MarshalIndent(currentSettings, "", " ") b, err := json.MarshalIndent(currentSettings, "", " ")
if err != nil { if err != nil {
fmt.Println("Error marshalling settings:", err) // Debug
return err return err
} }
return os.WriteFile(settingsFile, b, 0644) fmt.Println("Settings marshaled, writing to file...") // Log marshaling success
//return os.WriteFile(settingsFile, b, 0644)
err = os.WriteFile(settingsFile, b, 0644)
if err != nil {
log.Println("Error writing to file:", err) // Debug
} else {
log.Println("Settings saved successfully!") // Debug
}
return nil
} }
// GetSettings returns a copy of the current settings // GetSettings returns a copy of the current settings
@@ -132,6 +146,8 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
settingsLock.Lock() settingsLock.Lock()
defer settingsLock.Unlock() defer settingsLock.Unlock()
fmt.Println("Locked settings for update") // Log lock acquisition
old := currentSettings old := currentSettings
// If certain fields change, we mark reload needed // If certain fields change, we mark reload needed
@@ -154,11 +170,14 @@ func UpdateSettings(new AppSettings) (AppSettings, error) {
} }
currentSettings = new currentSettings = new
fmt.Println("New settings applied:", currentSettings) // Log settings applied
// persist to file // persist to file
if err := saveSettings(); err != nil { if err := saveSettings(); err != nil {
fmt.Println("Error saving settings:", err) // Log save error
return currentSettings, err return currentSettings, err
} }
fmt.Println("Settings saved to file successfully") // Log save success
return currentSettings, nil return currentSettings, nil
} }

View File

@@ -2,7 +2,6 @@ package web
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"strings" "strings"
@@ -58,6 +57,8 @@ func SummaryHandler(c *gin.Context) {
// UnbanIPHandler unbans a given IP in a specific jail. // UnbanIPHandler unbans a given IP in a specific jail.
func UnbanIPHandler(c *gin.Context) { func UnbanIPHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("UnbanIPHandler called (handlers.go)") // entry point
jail := c.Param("jail") jail := c.Param("jail")
ip := c.Param("ip") ip := c.Param("ip")
@@ -68,6 +69,7 @@ func UnbanIPHandler(c *gin.Context) {
}) })
return return
} }
fmt.Println(ip + " from jail " + jail + " unbanned successfully (handlers.go)")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "IP unbanned successfully", "message": "IP unbanned successfully",
}) })
@@ -92,6 +94,8 @@ func IndexHandler(c *gin.Context) {
// GetJailFilterConfigHandler returns the raw filter config for a given jail // GetJailFilterConfigHandler returns the raw filter config for a given jail
func GetJailFilterConfigHandler(c *gin.Context) { func GetJailFilterConfigHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("GetJailFilterConfigHandler called (handlers.go)") // entry point
jail := c.Param("jail") jail := c.Param("jail")
cfg, err := fail2ban.GetJailConfig(jail) cfg, err := fail2ban.GetJailConfig(jail)
if err != nil { if err != nil {
@@ -106,6 +110,8 @@ func GetJailFilterConfigHandler(c *gin.Context) {
// SetJailFilterConfigHandler overwrites the current filter config with new content // SetJailFilterConfigHandler overwrites the current filter config with new content
func SetJailFilterConfigHandler(c *gin.Context) { func SetJailFilterConfigHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("SetJailFilterConfigHandler called (handlers.go)") // entry point
jail := c.Param("jail") jail := c.Param("jail")
// Parse JSON body (containing the new filter content) // Parse JSON body (containing the new filter content)
@@ -140,23 +146,34 @@ func SetJailFilterConfigHandler(c *gin.Context) {
// GetSettingsHandler returns the entire AppSettings struct as JSON // GetSettingsHandler returns the entire AppSettings struct as JSON
func GetSettingsHandler(c *gin.Context) { func GetSettingsHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("GetSettingsHandler called (handlers.go)") // entry point
s := config.GetSettings() s := config.GetSettings()
c.JSON(http.StatusOK, s) c.JSON(http.StatusOK, s)
} }
// UpdateSettingsHandler updates the AppSettings from a JSON body // UpdateSettingsHandler updates the AppSettings from a JSON body
func UpdateSettingsHandler(c *gin.Context) { func UpdateSettingsHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("UpdateSettingsHandler called (handlers.go)") // entry point
var req config.AppSettings var req config.AppSettings
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON"}) fmt.Println("JSON binding error:", err) // Debug
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid JSON",
"details": err.Error(),
})
return return
} }
fmt.Println("JSON binding successful, updating settings (handlers.go)")
newSettings, err := config.UpdateSettings(req) newSettings, err := config.UpdateSettings(req)
if err != nil { if err != nil {
fmt.Println("Error updating settings:", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
} }
fmt.Println("Settings updated successfully (handlers.go)")
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "Settings updated", "message": "Settings updated",
@@ -167,9 +184,11 @@ func UpdateSettingsHandler(c *gin.Context) {
// ListFiltersHandler returns a JSON array of filter names // ListFiltersHandler returns a JSON array of filter names
// found as *.conf in /etc/fail2ban/filter.d // found as *.conf in /etc/fail2ban/filter.d
func ListFiltersHandler(c *gin.Context) { func ListFiltersHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("ListFiltersHandler called (handlers.go)") // entry point
dir := "/etc/fail2ban/filter.d" dir := "/etc/fail2ban/filter.d"
files, err := ioutil.ReadDir(dir) files, err := os.ReadDir(dir)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{
"error": "Failed to read filter directory: " + err.Error(), "error": "Failed to read filter directory: " + err.Error(),
@@ -189,6 +208,8 @@ func ListFiltersHandler(c *gin.Context) {
} }
func TestFilterHandler(c *gin.Context) { func TestFilterHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("TestFilterHandler called (handlers.go)") // entry point
var req struct { var req struct {
FilterName string `json:"filterName"` FilterName string `json:"filterName"`
LogLines []string `json:"logLines"` LogLines []string `json:"logLines"`
@@ -204,6 +225,8 @@ func TestFilterHandler(c *gin.Context) {
// ApplyFail2banSettings updates /etc/fail2ban/jail.local [DEFAULT] with our JSON // ApplyFail2banSettings updates /etc/fail2ban/jail.local [DEFAULT] with our JSON
func ApplyFail2banSettings(jailLocalPath string) error { func ApplyFail2banSettings(jailLocalPath string) error {
fmt.Println("----------------------------")
fmt.Println("ApplyFail2banSettings called (handlers.go)") // entry point
s := config.GetSettings() s := config.GetSettings()
// open /etc/fail2ban/jail.local, parse or do a simplistic approach: // open /etc/fail2ban/jail.local, parse or do a simplistic approach:
@@ -228,6 +251,9 @@ func ApplyFail2banSettings(jailLocalPath string) error {
// ReloadFail2banHandler reloads the Fail2ban service // ReloadFail2banHandler reloads the Fail2ban service
func ReloadFail2banHandler(c *gin.Context) { func ReloadFail2banHandler(c *gin.Context) {
fmt.Println("----------------------------")
fmt.Println("ApplyFail2banSettings called (handlers.go)") // entry point
// First we write our new settings to /etc/fail2ban/jail.local // First we write our new settings to /etc/fail2ban/jail.local
// if err := fail2ban.ApplyFail2banSettings("/etc/fail2ban/jail.local"); err != nil { // if err := fail2ban.ApplyFail2banSettings("/etc/fail2ban/jail.local"); err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) // c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
@@ -241,9 +267,9 @@ func ReloadFail2banHandler(c *gin.Context) {
} }
// We set reload done in config // We set reload done in config
//if err := config.MarkReloadDone(); err != nil { if err := config.MarkReloadDone(); err != nil {
// c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
// return return
//} }
c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"}) c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"})
} }

View File

@@ -103,8 +103,12 @@
<!-- Settings Section --> <!-- Settings Section -->
<div id="settingsSection" style="display: none;" class="container my-4"> <div id="settingsSection" style="display: none;" class="container my-4">
<h2>Settings</h2> <h2>Settings</h2>
<form onsubmit="saveSettings(event)"> <form onsubmit="saveSettings(event)">
<!-- General Settings Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">General Settings</legend>
<!-- Language Selection -->
<div class="mb-3"> <div class="mb-3">
<label for="languageSelect" class="form-label">Language</label> <label for="languageSelect" class="form-label">Language</label>
<select id="languageSelect" class="form-select" disabled> <select id="languageSelect" class="form-select" disabled>
@@ -112,13 +116,31 @@
<option value="de">Deutsch</option> <option value="de">Deutsch</option>
</select> </select>
</div> </div>
<div class="mb-3"> <!-- Debug Log Output -->
<label for="alertEmail" class="form-label">Alert Email</label> <div class="mb-3 form-check">
<input type="email" class="form-control" id="alertEmail"/> <input type="checkbox" class="form-check-input" id="debugMode">
<label for="debugMode" class="form-check-label">Enable Debug Log</label>
</div> </div>
</fieldset>
<!-- Alert Settings Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">Alert Settings</legend>
<!-- Source Email -->
<div class="mb-3">
<label for="sourceEmail" class="form-label">Source Email - Emails are sent from this address.</label>
<input type="email" class="form-control" id="sourceEmail"/>
</div>
<!-- Destination Email -->
<div class="mb-3">
<label for="destEmail" class="form-label">Destination Email - Where to sent the alert messages?</label>
<input type="email" class="form-control" id="destEmail" placeholder="e.g., alerts@swissmakers.ch" />
</div>
<!-- Alert Countries -->
<div class="mb-3"> <div class="mb-3">
<label for="alertCountries" class="form-label">Select alert Countries</label> <label for="alertCountries" class="form-label">Select alert Countries</label>
<p class="text-muted">Choose which country-IP bans should trigger an email. With CTRL, you can select multiple.</p> <p class="text-muted">Choose which country IP blocks should trigger an email. You can select multiple with CTRL.</p>
<select id="alertCountries" class="form-select" multiple size="7"> <select id="alertCountries" class="form-select" multiple size="7">
<option value="ALL">ALL (Every Country)</option> <option value="ALL">ALL (Every Country)</option>
<option value="CH">Switzerland (CH)</option> <option value="CH">Switzerland (CH)</option>
@@ -130,9 +152,41 @@
<!-- Maybe i will add more later.. --> <!-- Maybe i will add more later.. -->
</select> </select>
</div> </div>
<button type="submit" class="btn btn-primary">Save</button> </fieldset>
</form>
</div> <!-- Fail2Ban Configuration Group -->
<fieldset class="border p-3 rounded mb-4">
<legend class="w-auto px-2">Fail2Ban Configuration</legend>
<!-- Bantime Increment -->
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="bantimeIncrement" />
<label for="bantimeIncrement" class="form-check-label">Enable Bantime Increment</label>
</div>
<!-- Bantime -->
<div class="mb-3">
<label for="banTime" class="form-label">Default Bantime</label>
<input type="text" class="form-control" id="banTime" placeholder="e.g., 48h" />
</div>
<!-- Findtime -->
<div class="mb-3">
<label for="findTime" class="form-label">Default Findtime</label>
<input type="text" class="form-control" id="findTime" placeholder="e.g., 30m" />
</div>
<!-- Max Retry -->
<div class="mb-3">
<label for="maxRetry" class="form-label">Default Max Retry</label>
<input type="number" class="form-control" id="maxRetry" placeholder="Enter maximum retries" />
</div>
<!-- Ignore IPs -->
<div class="mb-3">
<label for="ignoreIP" class="form-label">Ignore IPs</label>
<textarea class="form-control" id="ignoreIP" rows="2" placeholder="Enter IPs to ignore, separated by spaces"></textarea>
</div>
</fieldset>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
<!-- ******************************************************************* --> <!-- ******************************************************************* -->
<!-- Footer --> <!-- Footer -->
@@ -464,10 +518,13 @@ function loadSettings() {
fetch('/api/settings') fetch('/api/settings')
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
// populate language, email, etc... // Get current general settings
document.getElementById('languageSelect').value = data.language || 'en'; document.getElementById('languageSelect').value = data.language || 'en';
document.getElementById('alertEmail').value = data.sender || ''; document.getElementById('debugMode').checked = data.debug || false;
// Get current alert settings
document.getElementById('sourceEmail').value = data.sender || '';
document.getElementById('destEmail').value = data.destemail || '';
// alertCountries multi // alertCountries multi
const select = document.getElementById('alertCountries'); const select = document.getElementById('alertCountries');
// clear selection // clear selection
@@ -486,6 +543,13 @@ function loadSettings() {
} }
} }
} }
// Get current Fail2Ban Configuration
document.getElementById('bantimeIncrement').checked = data.bantimeIncrement || false;
document.getElementById('banTime').value = data.bantime || '';
document.getElementById('findTime').value = data.findtime || '';
document.getElementById('maxRetry').value = data.maxretry || '';
document.getElementById('ignoreIP').value = data.ignoreip || '';
}) })
.catch(err => { .catch(err => {
alert('Error loading settings: ' + err); alert('Error loading settings: ' + err);
@@ -499,7 +563,9 @@ function saveSettings(e) {
showLoading(true); showLoading(true);
const lang = document.getElementById('languageSelect').value; const lang = document.getElementById('languageSelect').value;
const mail = document.getElementById('alertEmail').value; const debugMode = document.getElementById("debugMode").checked;
const srcmail = document.getElementById('sourceEmail').value;
const destmail = document.getElementById('destEmail').value;
const select = document.getElementById('alertCountries'); const select = document.getElementById('alertCountries');
let chosenCountries = []; let chosenCountries = [];
@@ -513,10 +579,23 @@ function saveSettings(e) {
chosenCountries = ["all"]; chosenCountries = ["all"];
} }
const bantimeinc = document.getElementById('bantimeIncrement').checked;
const bant = document.getElementById('banTime').value;
const findt = document.getElementById('findTime').value;
const maxre = parseInt(document.getElementById('maxRetry').value, 10) || 1; // Default to 1 (if parsing fails)
const ignip = document.getElementById('ignoreIP').value;
const body = { const body = {
language: lang, language: lang,
sender: mail, debug: debugMode,
alertCountries: chosenCountries sender: srcmail,
destemail: destmail,
alertCountries: chosenCountries,
bantimeIncrement: bantimeinc,
bantime: bant,
findtime: findt,
maxretry: maxre,
ignoreip: ignip
}; };
fetch('/api/settings', { fetch('/api/settings', {
@@ -527,7 +606,7 @@ function saveSettings(e) {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data.error) { if (data.error) {
alert('Error saving settings: ' + data.error); alert('Error saving settings: ' + data.error + data.details);
} else { } else {
//alert(data.message || 'Settings saved'); //alert(data.message || 'Settings saved');
console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded); console.log("Settings saved successfully. Reload needed? " + data.reloadNeeded);
@@ -642,7 +721,6 @@ function reloadFail2ban() {
if (data.error) { if (data.error) {
alert("Error: " + data.error); alert("Error: " + data.error);
} else { } else {
alert(data.message || "Fail2ban reloaded");
// Hide reload banner // Hide reload banner
document.getElementById('reloadBanner').style.display = 'none'; document.getElementById('reloadBanner').style.display = 'none';
// Refresh data // Refresh data