183 lines
4.8 KiB
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
|
|
}
|