// Fail2ban UI - A Swiss made, management interface for Fail2ban. // // Copyright (C) 2025 Swissmakers GmbH (https://swissmakers.ch) // // Licensed under the GNU General Public License, Version 3 (GPL-3.0) // You may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.gnu.org/licenses/gpl-3.0.en.html // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "fmt" "log" "net" "os" "strconv" "strings" "time" "github.com/gin-gonic/gin" "github.com/swissmakers/fail2ban-ui/internal/auth" "github.com/swissmakers/fail2ban-ui/internal/config" "github.com/swissmakers/fail2ban-ui/internal/fail2ban" "github.com/swissmakers/fail2ban-ui/internal/storage" "github.com/swissmakers/fail2ban-ui/pkg/web" ) func main() { // Get application settings from the config package. settings := config.GetSettings() if err := storage.Init(""); err != nil { log.Fatalf("Failed to initialise storage: %v", err) } defer func() { if err := storage.Close(); err != nil { log.Printf("warning: failed to close storage: %v", err) } }() if err := fail2ban.GetManager().ReloadFromSettings(settings); err != nil { log.Fatalf("failed to initialise fail2ban connectors: %v", err) } // Initialize OIDC authentication if enabled oidcConfig, err := config.GetOIDCConfigFromEnv() if err != nil { log.Fatalf("failed to load OIDC configuration: %v", err) } if oidcConfig != nil && oidcConfig.Enabled { // Initialize session secret if err := auth.InitializeSessionSecret(oidcConfig.SessionSecret); err != nil { log.Fatalf("failed to initialize session secret: %v", err) } // Initialize OIDC client if _, err := auth.InitializeOIDC(oidcConfig); err != nil { log.Fatalf("failed to initialize OIDC: %v", err) } log.Println("OIDC authentication enabled") } // Set Gin mode based on the debug flag in settings. if settings.Debug { gin.SetMode(gin.DebugMode) } else { gin.SetMode(gin.ReleaseMode) } // Create a new Gin router. router := gin.Default() serverPort := strconv.Itoa(int(settings.Port)) // Get bind address from environment variable, defaulting to 0.0.0.0 bindAddress, _ := config.GetBindAddressFromEnv() serverAddr := net.JoinHostPort(bindAddress, serverPort) // Load HTML templates depending on whether the application is running inside a container. _, container := os.LookupEnv("CONTAINER") if container { // In container, templates are assumed to be in /app/templates router.LoadHTMLGlob("/app/templates/*") router.Static("/locales", "/app/locales") router.Static("/static", "/app/static") } else { // When running locally, load templates from pkg/web/templates router.LoadHTMLGlob("pkg/web/templates/*") router.Static("/locales", "./internal/locales") router.Static("/static", "./pkg/web/static") } // Initialize WebSocket hub wsHub := web.NewHub() go wsHub.Run() // Setup console log writer to capture stdout and send via WebSocket web.SetupConsoleLogWriter(wsHub) // Update enabled state based on current settings web.UpdateConsoleLogEnabled() // Register callback to update console log state when settings change config.SetUpdateConsoleLogStateFunc(func(enabled bool) { web.SetConsoleLogEnabled(enabled) }) // Register all application routes, including the static files and templates. web.RegisterRoutes(router, wsHub) // Check if LOTR mode is active isLOTRMode := isLOTRModeActive(settings.AlertCountries) printWelcomeBanner(bindAddress, serverPort, isLOTRMode) if isLOTRMode { log.Println("--- Middle-earth Security Realm activated ---") log.Println("🎭 LOTR Mode: The guardians of Middle-earth stand ready!") } else { log.Println("--- Fail2Ban-UI started in", gin.Mode(), "mode ---") } log.Printf("Server listening on %s:%s.\n", bindAddress, serverPort) // Start the server on the configured address and port. if err := router.Run(serverAddr); err != nil { log.Fatalf("Could not start server: %v\n", err) } } func isLOTRModeActive(alertCountries []string) bool { if len(alertCountries) == 0 { return false } for _, country := range alertCountries { if strings.EqualFold(country, "LOTR") { return true } } return false } // printWelcomeBanner prints the Tux banner with startup info. func printWelcomeBanner(bindAddress, appPort string, isLOTRMode bool) { greeting := getGreeting() if isLOTRMode { const lotrBanner = ` .--. |o_o | %s |:_/ | // \ \ (| | ) /'\_ _/'\ \___)=(___/ Middle-earth Security Realm - LOTR Mode Activated ══════════════════════════════════════════════════ ⚔️ The guardians of Middle-earth stand ready! ⚔️ Developers: https://swissmakers.ch Mode: %s Listening on: http://%s:%s ══════════════════════════════════════════════════ ` fmt.Printf(lotrBanner, greeting, gin.Mode(), bindAddress, appPort) } else { const tuxBanner = ` .--. |o_o | %s |:_/ | // \ \ (| | ) /'\_ _/'\ \___)=(___/ Fail2Ban UI - A Swissmade Management Interface ---------------------------------------------- Developers: https://swissmakers.ch Mode: %s Listening on: http://%s:%s ---------------------------------------------- ` fmt.Printf(tuxBanner, greeting, gin.Mode(), bindAddress, appPort) } } // getGreeting returns a friendly greeting based on the time of day. func getGreeting() string { hour := time.Now().Hour() switch { case hour < 12: return "Good morning!" case hour < 18: return "Good afternoon!" default: return "Good evening!" } }