Files
Charon/backend/internal/caddy/validator.go

183 lines
4.8 KiB
Go

package caddy
import (
"encoding/json"
"fmt"
"net"
"strconv"
"strings"
"github.com/Wikid82/charon/backend/internal/logger"
)
// Validate performs pre-flight validation on a Caddy config before applying it.
func Validate(cfg *Config) error {
if cfg == nil {
return fmt.Errorf("config cannot be nil")
}
if cfg.Apps.HTTP == nil {
return nil // Empty config is valid
}
// Track seen hosts with their path configuration
// Value: "with_paths" or "without_paths"
seenHosts := make(map[string]string)
for serverName, server := range cfg.Apps.HTTP.Servers {
if len(server.Listen) == 0 {
return fmt.Errorf("server %s has no listen addresses", serverName)
}
// Validate listen addresses
for _, addr := range server.Listen {
if err := validateListenAddr(addr); err != nil {
return fmt.Errorf("invalid listen address %s in server %s: %w", addr, serverName, err)
}
}
// Validate routes
for i, route := range server.Routes {
if err := validateRoute(route, seenHosts); err != nil {
return fmt.Errorf("invalid route %d in server %s: %w", i, serverName, err)
}
}
}
// Validate JSON marshalling works
if _, err := jsonMarshalValidate(cfg); err != nil {
return fmt.Errorf("config cannot be marshalled to JSON: %w", err)
}
return nil
}
// allow tests to override JSON marshalling to simulate errors
var jsonMarshalValidate = json.Marshal
func validateListenAddr(addr string) error {
// Strip network type prefix if present (tcp/, udp/)
if idx := strings.Index(addr, "/"); idx != -1 {
addr = addr[idx+1:]
}
// Parse host:port
host, portStr, err := net.SplitHostPort(addr)
if err != nil {
return fmt.Errorf("invalid address format: %w", err)
}
// Validate port
port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("invalid port: %w", err)
}
if port < 1 || port > 65535 {
return fmt.Errorf("port %d out of range (1-65535)", port)
}
// Validate host (allow empty for wildcard binding)
if host != "" && net.ParseIP(host) == nil {
return fmt.Errorf("invalid IP address: %s", host)
}
return nil
}
func validateRoute(route *Route, seenHosts map[string]string) error {
if len(route.Handle) == 0 {
return fmt.Errorf("route has no handlers")
}
// Check for duplicate host matchers with incompatible path configurations
// Allow emergency+main pattern: one route with paths, one without
for _, match := range route.Match {
hasPaths := len(match.Path) > 0
pathConfig := "without_paths"
if hasPaths {
pathConfig = "with_paths"
}
for _, host := range match.Host {
logger.Log().WithFields(map[string]any{
"host": host,
"has_paths": hasPaths,
"paths": match.Path,
"path_config": pathConfig,
}).Debug("[VALIDATOR] Checking host matcher")
if existingConfig, seen := seenHosts[host]; seen {
// Host already seen - check if path configs are compatible
if existingConfig == pathConfig {
// Same path configuration = true duplicate
if pathConfig == "with_paths" {
logger.Log().WithField("host", host).Error("[VALIDATOR] Duplicate host with paths")
return fmt.Errorf("duplicate host with paths: %s", host)
}
logger.Log().WithField("host", host).Error("[VALIDATOR] Duplicate host without paths")
return fmt.Errorf("duplicate host without paths: %s", host)
}
// Different path configuration = emergency+main pattern (ALLOWED)
logger.Log().WithFields(map[string]any{
"host": host,
"existing_config": existingConfig,
"new_config": pathConfig,
}).Debug("[VALIDATOR] Allowing emergency+main pattern")
continue
}
seenHosts[host] = pathConfig
}
}
// Validate handlers
for i, handler := range route.Handle {
if err := validateHandler(handler); err != nil {
return fmt.Errorf("invalid handler %d: %w", i, err)
}
}
return nil
}
func validateHandler(handler Handler) error {
handlerType, ok := handler["handler"].(string)
if !ok {
return fmt.Errorf("handler missing 'handler' field")
}
switch handlerType {
case "reverse_proxy":
return validateReverseProxy(handler)
case "file_server", "static_response":
return nil // Accept other common handlers
default:
// Unknown handlers are allowed (Caddy is extensible)
return nil
}
}
func validateReverseProxy(handler Handler) error {
upstreams, ok := handler["upstreams"].([]map[string]any)
if !ok {
return fmt.Errorf("reverse_proxy missing upstreams")
}
if len(upstreams) == 0 {
return fmt.Errorf("reverse_proxy has no upstreams")
}
for i, upstream := range upstreams {
dial, ok := upstream["dial"].(string)
if !ok || dial == "" {
return fmt.Errorf("upstream %d missing dial address", i)
}
// Validate dial address format (host:port)
if _, _, err := net.SplitHostPort(dial); err != nil {
return fmt.Errorf("upstream %d has invalid dial address %s: %w", i, dial, err)
}
}
return nil
}