diff --git a/internal/config/settings.go b/internal/config/settings.go index fb3ab0f..7a05604 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -28,22 +28,32 @@ import ( "sync" ) -// AppSettings holds both the UI settings (like language) and -// relevant Fail2ban jail/local config options. -type AppSettings struct { - Language string `json:"language"` - Debug bool `json:"debug"` - ReloadNeeded bool `json:"reloadNeeded"` - AlertCountries []string `json:"alertCountries"` +// SMTPSettings holds the SMTP server configuration for sending alert emails +type SMTPSettings struct { + Host string `json:"host"` + Port int `json:"port"` + Username string `json:"username"` + Password string `json:"password"` + From string `json:"from"` + UseTLS bool `json:"useTLS"` +} - // These mirror some Fail2ban [DEFAULT] section parameters from jail.local +// AppSettings holds the main UI settings and Fail2ban configuration +type AppSettings struct { + Language string `json:"language"` + Debug bool `json:"debug"` + ReloadNeeded bool `json:"reloadNeeded"` + AlertCountries []string `json:"alertCountries"` + SMTP SMTPSettings `json:"smtp"` + + // Fail2Ban [DEFAULT] section values from jail.local BantimeIncrement bool `json:"bantimeIncrement"` IgnoreIP string `json:"ignoreip"` Bantime string `json:"bantime"` Findtime string `json:"findtime"` Maxretry int `json:"maxretry"` Destemail string `json:"destemail"` - Sender string `json:"sender"` + //Sender string `json:"sender"` } // init paths to key-files @@ -88,7 +98,7 @@ func setDefaults() { currentSettings.Language = "en" } if currentSettings.AlertCountries == nil { - currentSettings.AlertCountries = []string{"all"} + currentSettings.AlertCountries = []string{"ALL"} } if currentSettings.Bantime == "" { currentSettings.Bantime = "48h" @@ -100,10 +110,25 @@ func setDefaults() { currentSettings.Maxretry = 3 } if currentSettings.Destemail == "" { - currentSettings.Destemail = "alerts@swissmakers.ch" + currentSettings.Destemail = "alerts@example.com" } - if currentSettings.Sender == "" { - currentSettings.Sender = "noreply@swissmakers.ch" + if currentSettings.SMTP.Host == "" { + currentSettings.SMTP.Host = "smtp.office365.com" + } + if currentSettings.SMTP.Port == 0 { + currentSettings.SMTP.Port = 587 + } + if currentSettings.SMTP.Username == "" { + currentSettings.SMTP.Username = "noreply@swissmakers.ch" + } + if currentSettings.SMTP.Password == "" { + currentSettings.SMTP.Password = "password" + } + if currentSettings.SMTP.From == "" { + currentSettings.SMTP.From = "noreply@swissmakers.ch" + } + if !currentSettings.SMTP.UseTLS { + currentSettings.SMTP.UseTLS = true } if currentSettings.IgnoreIP == "" { currentSettings.IgnoreIP = "127.0.0.1/8 ::1" @@ -151,9 +176,9 @@ func initializeFromJailFile() error { if val, ok := settings["destemail"]; ok { currentSettings.Destemail = val } - if val, ok := settings["sender"]; ok { + /*if val, ok := settings["sender"]; ok { currentSettings.Sender = val - } + }*/ return nil } @@ -379,9 +404,10 @@ func UpdateSettings(new AppSettings) (AppSettings, error) { old.IgnoreIP != new.IgnoreIP || old.Bantime != new.Bantime || old.Findtime != new.Findtime || - old.Maxretry != new.Maxretry || + //old.Maxretry != new.Maxretry || old.Destemail != new.Destemail || - old.Sender != new.Sender { + //old.Sender != new.Sender { + old.Maxretry != new.Maxretry { new.ReloadNeeded = true } else { // preserve previous ReloadNeeded if it was already true diff --git a/pkg/web/handlers.go b/pkg/web/handlers.go index 2351b11..61ee32d 100644 --- a/pkg/web/handlers.go +++ b/pkg/web/handlers.go @@ -17,13 +17,14 @@ package web import ( - "bytes" + "crypto/tls" + "errors" "fmt" "log" "net" "net/http" + "net/smtp" "os" - "os/exec" "strings" "time" @@ -195,40 +196,6 @@ func shouldAlertForCountry(country string, alertCountries []string) bool { return false } -// sendBanAlert sends an email notification for a banned IP. -func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, settings config.AppSettings) error { - // Construct email content - emailBody := fmt.Sprintf(`Subject: [Fail2Ban] %s: banned %s from %s -Date: `+"`LC_ALL=C date +\"%%a, %%d %%h %%Y %%T %%z\"`"+` -From: %s <%s> -To: %s - -Hi, - -The IP %s has just been banned by Fail2Ban after %s attempts against %s. - -📍 Country: %s -🔍 Whois Info: -%s - -📄 Log Entries: -%s - -Best Regards, -Fail2Ban -`, jail, ip, hostname, settings.Sender, settings.Sender, settings.Destemail, ip, failures, jail, country, whois, logs) - - // Use msmtp or sendmail to send email - cmd := exec.Command("/usr/sbin/sendmail", "-f", settings.Sender, settings.Destemail) - cmd.Stdin = bytes.NewBufferString(emailBody) - - if err := cmd.Run(); err != nil { - return fmt.Errorf("failed to send email: %w", err) - } - - return nil -} - func sortByTimeDesc(events []fail2ban.BanEvent) { for i := 0; i < len(events); i++ { for j := i + 1; j < len(events); j++ { @@ -395,7 +362,7 @@ func ApplyFail2banSettings(jailLocalPath string) error { fmt.Sprintf("findtime = %s", s.Findtime), fmt.Sprintf("maxretry = %d", s.Maxretry), fmt.Sprintf("destemail = %s", s.Destemail), - fmt.Sprintf("sender = %s", s.Sender), + //fmt.Sprintf("sender = %s", s.Sender), "", } content := strings.Join(newLines, "\n") @@ -427,3 +394,200 @@ func ReloadFail2banHandler(c *gin.Context) { } c.JSON(http.StatusOK, gin.H{"message": "Fail2ban reloaded successfully"}) } + +// ******************************************************************* +// * Unified Email Sending Function : * +// ******************************************************************* +func sendEmail(to, subject, body string, settings config.AppSettings) error { + // Validate SMTP settings + if settings.SMTP.Host == "" || settings.SMTP.Username == "" || settings.SMTP.Password == "" || settings.SMTP.From == "" { + return errors.New("SMTP settings are incomplete. Please configure all required fields") + } + + // Format message with **correct HTML headers** + message := fmt.Sprintf("From: %s\nTo: %s\nSubject: %s\n"+ + "MIME-Version: 1.0\nContent-Type: text/html; charset=\"UTF-8\"\n\n%s", + settings.SMTP.From, to, subject, body) + msg := []byte(message) + + // SMTP Connection Config + smtpHost := settings.SMTP.Host + smtpPort := settings.SMTP.Port + auth := LoginAuth(settings.SMTP.Username, settings.SMTP.Password) + smtpAddr := fmt.Sprintf("%s:%d", smtpHost, smtpPort) + + // **Choose Connection Type** + if smtpPort == 465 { + // SMTPS (Implicit TLS) - Not supported at the moment. + tlsConfig := &tls.Config{ServerName: smtpHost} + conn, err := tls.Dial("tcp", smtpAddr, tlsConfig) + if err != nil { + return fmt.Errorf("failed to connect via TLS: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, smtpHost) + if err != nil { + return fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Quit() + + if err := client.Auth(auth); err != nil { + return fmt.Errorf("SMTP authentication failed: %w", err) + } + + return sendSMTPMessage(client, settings.SMTP.From, to, msg) + + } else if smtpPort == 587 { + // STARTTLS (Explicit TLS) + conn, err := net.Dial("tcp", smtpAddr) + if err != nil { + return fmt.Errorf("failed to connect to SMTP server: %w", err) + } + defer conn.Close() + + client, err := smtp.NewClient(conn, smtpHost) + if err != nil { + return fmt.Errorf("failed to create SMTP client: %w", err) + } + defer client.Quit() + + // Start TLS Upgrade + tlsConfig := &tls.Config{ServerName: smtpHost} + if err := client.StartTLS(tlsConfig); err != nil { + return fmt.Errorf("failed to start TLS: %w", err) + } + + if err := client.Auth(auth); err != nil { + return fmt.Errorf("SMTP authentication failed: %w", err) + } + + return sendSMTPMessage(client, settings.SMTP.From, to, msg) + } + + return errors.New("unsupported SMTP port. Use 587 (STARTTLS) or 465 (SMTPS)") +} + +// Helper Function to Send SMTP Message +func sendSMTPMessage(client *smtp.Client, from, to string, msg []byte) error { + // Set sender & recipient + if err := client.Mail(from); err != nil { + return fmt.Errorf("failed to set sender: %w", err) + } + if err := client.Rcpt(to); err != nil { + return fmt.Errorf("failed to set recipient: %w", err) + } + + // Send email body + wc, err := client.Data() + if err != nil { + return fmt.Errorf("failed to start data command: %w", err) + } + defer wc.Close() + + if _, err = wc.Write(msg); err != nil { + return fmt.Errorf("failed to write email content: %w", err) + } + + // Close connection + client.Quit() + return nil +} + +// ******************************************************************* +// * sendBanAlert Function : * +// ******************************************************************* +func sendBanAlert(ip, jail, hostname, failures, whois, logs, country string, settings config.AppSettings) error { + subject := fmt.Sprintf("[Fail2Ban] %s: banned %s from %s", jail, ip, hostname) + + // Ensure HTML email format + body := fmt.Sprintf(` + +
+ +A new IP has been banned due to excessive failed login attempts.
+📌 Banned IP: %s
+🛡️ Jail Name: %s
+🏠 Hostname: %s
+🚫 Failed Attempts: %s
+🌍 Country: %s
+%s+
%s+ +