From c6771be7a8a5a90401cedcd10f134f7084b95090 Mon Sep 17 00:00:00 2001 From: Wikid82 Date: Sat, 22 Nov 2025 19:26:38 -0500 Subject: [PATCH] feat: add default 404 page for unknown hosts --- .../api/handlers/proxy_host_handler_test.go | 4 ++-- backend/internal/api/routes/routes.go | 2 +- backend/internal/caddy/client_test.go | 2 +- backend/internal/caddy/config.go | 19 ++++++++++++++++--- backend/internal/caddy/config_test.go | 14 +++++++------- backend/internal/caddy/manager.go | 18 ++++++++++-------- backend/internal/caddy/manager_test.go | 10 +++++----- backend/internal/caddy/types.go | 16 ++++++++++++++++ backend/internal/caddy/validator_test.go | 2 +- 9 files changed, 59 insertions(+), 28 deletions(-) diff --git a/backend/internal/api/handlers/proxy_host_handler_test.go b/backend/internal/api/handlers/proxy_host_handler_test.go index f66a6b8b..4efd6eab 100644 --- a/backend/internal/api/handlers/proxy_host_handler_test.go +++ b/backend/internal/api/handlers/proxy_host_handler_test.go @@ -111,7 +111,7 @@ func TestProxyHostErrors(t *testing.T) { // Setup Caddy Manager tmpDir := t.TempDir() client := caddy.NewClient(caddyServer.URL) - manager := caddy.NewManager(client, db, tmpDir) + manager := caddy.NewManager(client, db, tmpDir, "") // Setup Handler h := NewProxyHostHandler(db, manager) @@ -284,7 +284,7 @@ func TestProxyHostWithCaddyIntegration(t *testing.T) { // Setup Caddy Manager tmpDir := t.TempDir() client := caddy.NewClient(caddyServer.URL) - manager := caddy.NewManager(client, db, tmpDir) + manager := caddy.NewManager(client, db, tmpDir, "") // Setup Handler h := NewProxyHostHandler(db, manager) diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 3b7bf14e..38664818 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -132,7 +132,7 @@ func Register(router *gin.Engine, db *gorm.DB, cfg config.Config) error { // Caddy Manager caddyClient := caddy.NewClient(cfg.CaddyAdminAPI) - caddyManager := caddy.NewManager(caddyClient, db, cfg.CaddyConfigDir) + caddyManager := caddy.NewManager(caddyClient, db, cfg.CaddyConfigDir, cfg.FrontendDir) proxyHostHandler := handlers.NewProxyHostHandler(db, caddyManager) proxyHostHandler.RegisterRoutes(api) diff --git a/backend/internal/caddy/client_test.go b/backend/internal/caddy/client_test.go index 7e88857e..7701465c 100644 --- a/backend/internal/caddy/client_test.go +++ b/backend/internal/caddy/client_test.go @@ -30,7 +30,7 @@ func TestClient_Load_Success(t *testing.T) { ForwardPort: 8080, Enabled: true, }, - }, "/tmp/caddy-data", "admin@example.com") + }, "/tmp/caddy-data", "admin@example.com", "") err := client.Load(context.Background(), config) require.NoError(t, err) diff --git a/backend/internal/caddy/config.go b/backend/internal/caddy/config.go index 28c7d12c..4828ce6d 100644 --- a/backend/internal/caddy/config.go +++ b/backend/internal/caddy/config.go @@ -10,7 +10,7 @@ import ( // GenerateConfig creates a Caddy JSON configuration from proxy hosts. // This is the core transformation layer from our database model to Caddy config. -func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string) (*Config, error) { +func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail string, frontendDir string) (*Config, error) { // Define log file paths // We assume storageDir is like ".../data/caddy/data", so we go up to ".../data/logs" // storageDir is .../data/caddy/data @@ -71,11 +71,11 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin } } - if len(hosts) == 0 { + if len(hosts) == 0 && frontendDir == "" { return config, nil } - // We already initialized srv0 above, so we just append routes to it + // Initialize routes slice routes := make([]*Route, 0) for _, host := range hosts { @@ -145,6 +145,19 @@ func GenerateConfig(hosts []models.ProxyHost, storageDir string, acmeEmail strin routes = append(routes, route) } + // Add catch-all 404 handler + // This matches any request that wasn't handled by previous routes + if frontendDir != "" { + catchAllRoute := &Route{ + Handle: []Handler{ + RewriteHandler("/unknown.html"), + FileServerHandler(frontendDir), + }, + Terminal: true, + } + routes = append(routes, catchAllRoute) + } + config.Apps.HTTP.Servers["cpm_server"] = &Server{ Listen: []string{":80", ":443"}, Routes: routes, diff --git a/backend/internal/caddy/config_test.go b/backend/internal/caddy/config_test.go index 97e19a47..4b50cb2a 100644 --- a/backend/internal/caddy/config_test.go +++ b/backend/internal/caddy/config_test.go @@ -9,7 +9,7 @@ import ( ) func TestGenerateConfig_Empty(t *testing.T) { - config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com") + config, err := GenerateConfig([]models.ProxyHost{}, "/tmp/caddy-data", "admin@example.com", "") require.NoError(t, err) require.NotNil(t, config) require.NotNil(t, config.Apps.HTTP) @@ -31,7 +31,7 @@ func TestGenerateConfig_SingleHost(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") require.NoError(t, err) require.NotNil(t, config) require.NotNil(t, config.Apps.HTTP) @@ -71,7 +71,7 @@ func TestGenerateConfig_MultipleHosts(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") require.NoError(t, err) require.Len(t, config.Apps.HTTP.Servers["cpm_server"].Routes, 2) } @@ -88,7 +88,7 @@ func TestGenerateConfig_WebSocketEnabled(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") require.NoError(t, err) route := config.Apps.HTTP.Servers["cpm_server"].Routes[0] @@ -109,14 +109,14 @@ func TestGenerateConfig_EmptyDomain(t *testing.T) { }, } - _, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + _, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") require.Error(t, err) require.Contains(t, err.Error(), "empty domain") } func TestGenerateConfig_Logging(t *testing.T) { hosts := []models.ProxyHost{} - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") require.NoError(t, err) // Verify logging configuration @@ -154,7 +154,7 @@ func TestGenerateConfig_Advanced(t *testing.T) { }, } - config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + config, err := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") require.NoError(t, err) require.NotNil(t, config) diff --git a/backend/internal/caddy/manager.go b/backend/internal/caddy/manager.go index 2d518921..70096d7f 100644 --- a/backend/internal/caddy/manager.go +++ b/backend/internal/caddy/manager.go @@ -17,17 +17,19 @@ import ( // Manager orchestrates Caddy configuration lifecycle: generate, validate, apply, rollback. type Manager struct { - client *Client - db *gorm.DB - configDir string + client *Client + db *gorm.DB + configDir string + frontendDir string } // NewManager creates a configuration manager. -func NewManager(client *Client, db *gorm.DB, configDir string) *Manager { +func NewManager(client *Client, db *gorm.DB, configDir string, frontendDir string) *Manager { return &Manager{ - client: client, - db: db, - configDir: configDir, + client: client, + db: db, + configDir: configDir, + frontendDir: frontendDir, } } @@ -47,7 +49,7 @@ func (m *Manager) ApplyConfig(ctx context.Context) error { } // Generate Caddy config - config, err := GenerateConfig(hosts, filepath.Join(m.configDir, "data"), acmeEmail) + config, err := GenerateConfig(hosts, filepath.Join(m.configDir, "data"), acmeEmail, m.frontendDir) if err != nil { return fmt.Errorf("generate config: %w", err) } diff --git a/backend/internal/caddy/manager_test.go b/backend/internal/caddy/manager_test.go index 59120db4..f025cf66 100644 --- a/backend/internal/caddy/manager_test.go +++ b/backend/internal/caddy/manager_test.go @@ -45,7 +45,7 @@ func TestManager_ApplyConfig(t *testing.T) { // Setup Manager tmpDir := t.TempDir() client := NewClient(caddyServer.URL) - manager := NewManager(client, db, tmpDir) + manager := NewManager(client, db, tmpDir, "") // Create a host host := models.ProxyHost{ @@ -82,7 +82,7 @@ func TestManager_ApplyConfig_Failure(t *testing.T) { // Setup Manager tmpDir := t.TempDir() client := NewClient(caddyServer.URL) - manager := NewManager(client, db, tmpDir) + manager := NewManager(client, db, tmpDir, "") // Create a host host := models.ProxyHost{ @@ -117,7 +117,7 @@ func TestManager_Ping(t *testing.T) { defer caddyServer.Close() client := NewClient(caddyServer.URL) - manager := NewManager(client, nil, "") + manager := NewManager(client, nil, "", "") err := manager.Ping(context.Background()) assert.NoError(t, err) @@ -136,7 +136,7 @@ func TestManager_GetCurrentConfig(t *testing.T) { defer caddyServer.Close() client := NewClient(caddyServer.URL) - manager := NewManager(client, nil, "") + manager := NewManager(client, nil, "", "") config, err := manager.GetCurrentConfig(context.Background()) assert.NoError(t, err) @@ -161,7 +161,7 @@ func TestManager_RotateSnapshots(t *testing.T) { require.NoError(t, db.AutoMigrate(&models.ProxyHost{}, &models.Setting{}, &models.CaddyConfig{})) client := NewClient(caddyServer.URL) - manager := NewManager(client, db, tmpDir) + manager := NewManager(client, db, tmpDir, "") // Create 15 dummy config files for i := 0; i < 15; i++ { diff --git a/backend/internal/caddy/types.go b/backend/internal/caddy/types.go index 5a8279d4..70b0bf33 100644 --- a/backend/internal/caddy/types.go +++ b/backend/internal/caddy/types.go @@ -141,6 +141,22 @@ func BlockExploitsHandler() Handler { } } +// RewriteHandler creates a rewrite handler. +func RewriteHandler(uri string) Handler { + return Handler{ + "handler": "rewrite", + "uri": uri, + } +} + +// FileServerHandler creates a file_server handler. +func FileServerHandler(root string) Handler { + return Handler{ + "handler": "file_server", + "root": root, + } +} + // TLSApp configures the TLS app for certificate management. type TLSApp struct { Automation *AutomationConfig `json:"automation,omitempty"` diff --git a/backend/internal/caddy/validator_test.go b/backend/internal/caddy/validator_test.go index 477152c2..72a928c9 100644 --- a/backend/internal/caddy/validator_test.go +++ b/backend/internal/caddy/validator_test.go @@ -25,7 +25,7 @@ func TestValidate_ValidConfig(t *testing.T) { }, } - config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com") + config, _ := GenerateConfig(hosts, "/tmp/caddy-data", "admin@example.com", "") err := Validate(config) require.NoError(t, err) }