package config import ( "fmt" "net" "net/url" "regexp" "strings" ) func Validate(cfg *Config) error { if cfg == nil { return fmt.Errorf("validate configuration: nil config") } if cfg.Version != 1 { return fmt.Errorf("validate configuration: version must be 1") } listeners := []struct { name string item Listener }{ {name: "gateway", item: cfg.Gateway}, {name: "distribution", item: cfg.Distribution.Listener}, {name: "admin", item: cfg.Admin}, } for _, listener := range listeners { if err := validateListener(listener.name, listener.item, cfg.Security); err != nil { return err } } for name, upstream := range cfg.Upstreams { if err := validateUpstream(name, upstream); err != nil { return err } } seen := make(map[string]struct{}, len(cfg.Routing)) for index, route := range cfg.Routing { if route.Name == "" { return fmt.Errorf("validate routing[%d]: name is required", index) } if _, ok := seen[route.Name]; ok { return fmt.Errorf("validate routing %q: duplicate name", route.Name) } seen[route.Name] = struct{}{} if route.Match.HostRegex != "" { if _, err := regexp.Compile(route.Match.HostRegex); err != nil { return fmt.Errorf("validate routing %q hostRegex: %w", route.Name, err) } } if route.Match.PathRegex != "" { if _, err := regexp.Compile(route.Match.PathRegex); err != nil { return fmt.Errorf("validate routing %q pathRegex: %w", route.Name, err) } } for _, upstream := range route.Upstreams { if _, ok := cfg.Upstreams[upstream]; !ok { return fmt.Errorf("validate routing %q: upstream %q does not exist", route.Name, upstream) } } if route.Strategy.Type == "sequential" && route.Strategy.SwitchAfterEmptyFetch <= 0 { return fmt.Errorf("validate routing %q: switchAfterEmptyFetch must be greater than zero", route.Name) } if route.OnUnavailable.Action == "" { return fmt.Errorf("validate routing %q: onUnavailable.action is required", route.Name) } } if cfg.Distribution.Enabled { if cfg.Distribution.Extraction.MaxCountPerRequest <= 0 { return fmt.Errorf("validate distribution: maxCountPerRequest must be greater than zero") } if cfg.Distribution.Extraction.Fulfillment != "partial" && cfg.Distribution.Extraction.Fulfillment != "allOrNothing" { return fmt.Errorf("validate distribution: fulfillment must be partial or allOrNothing") } } return nil } func validateListener(name string, listener Listener, security Security) error { if !listener.Enabled { return nil } if listener.Listen == "" { return fmt.Errorf("validate %s: listen is required", name) } host, _, err := net.SplitHostPort(listener.Listen) if err != nil { return fmt.Errorf("validate %s listen: %w", name, err) } if security.RequireProtectionOnPublicListen && isPublicHost(host) && listener.Auth.Mode == "none" && len(listener.Access.AllowCIDRs) == 0 { return fmt.Errorf("validate %s: unprotected public listener is forbidden", name) } for _, cidr := range listener.Access.AllowCIDRs { if _, _, err := net.ParseCIDR(cidr); err != nil { return fmt.Errorf("validate %s allowCIDRs %q: %w", name, cidr, err) } } return nil } func validateUpstream(name string, upstream Upstream) error { if !upstream.Enabled { return nil } if upstream.Pool.MaxSize <= 0 { return fmt.Errorf("validate upstream %q: pool.maxSize must be greater than zero", name) } if upstream.Fetch.MaxTotal > 0 && upstream.Fetch.MaxTotal < upstream.Pool.MaxSize { return fmt.Errorf("validate upstream %q: fetch.maxTotal cannot be lower than pool.maxSize", name) } if upstream.Capacity.MaxConcurrencyPerProxy <= 0 { return fmt.Errorf("validate upstream %q: maxConcurrencyPerProxy must be greater than zero", name) } if upstream.Lifecycle.TTL > 0 && upstream.Lifecycle.AllocationSafetyMargin >= upstream.Lifecycle.TTL { return fmt.Errorf("validate upstream %q: allocationSafetyMargin must be lower than ttl", name) } if upstream.Fetch.RequestInterval < 0 || upstream.Fetch.MaxInFlight <= 0 || upstream.Fetch.MaxAttempts <= 0 { return fmt.Errorf("validate upstream %q: fetch limits must be positive", name) } if upstream.API.URL != "" { parsed, err := url.Parse(upstream.API.URL) if err != nil || parsed.Scheme == "" || parsed.Host == "" { return fmt.Errorf("validate upstream %q: api.url is invalid", name) } } if err := validateProviderAuth(name, upstream.API.Auth); err != nil { return err } if err := validateProxyAuth(name, upstream.ProxyAuth); err != nil { return err } if len(upstream.Exposure) == 0 { return fmt.Errorf("validate upstream %q: exposure is required", name) } for _, exposure := range upstream.Exposure { if exposure != "gateway" && exposure != "extract" { return fmt.Errorf("validate upstream %q: unsupported exposure %q", name, exposure) } } return nil } func validateProviderAuth(upstream string, auth ProviderAuth) error { switch auth.Type { case "", "none": return nil case "basic": if auth.Username == "" || (auth.Password == "" && auth.PasswordFile == "") { return fmt.Errorf("validate upstream %q: basic api.auth requires username and password", upstream) } case "bearer": if auth.Token == "" && auth.TokenFile == "" { return fmt.Errorf("validate upstream %q: bearer api.auth requires token", upstream) } case "apiKey": if auth.Location != "header" && auth.Location != "query" { return fmt.Errorf("validate upstream %q: apiKey location must be header or query", upstream) } if auth.Name == "" || (auth.Value == "" && auth.ValueFile == "") { return fmt.Errorf("validate upstream %q: apiKey requires name and value", upstream) } default: return fmt.Errorf("validate upstream %q: unsupported api.auth type %q", upstream, auth.Type) } return nil } func validateProxyAuth(upstream string, auth ProxyAuth) error { switch auth.Type { case "response", "ipWhitelist": return nil case "static": if auth.Username == "" || (auth.Password == "" && auth.PasswordFile == "") { return fmt.Errorf("validate upstream %q: static proxyAuth requires username and password", upstream) } case "": return fmt.Errorf("validate upstream %q: proxyAuth.type is required", upstream) default: return fmt.Errorf("validate upstream %q: unsupported proxyAuth type %q", upstream, auth.Type) } return nil } func isPublicHost(host string) bool { host = strings.Trim(host, "[]") if host == "localhost" { return false } ip := net.ParseIP(host) if ip == nil { return true } return !ip.IsLoopback() }