package config import ( "fmt" "math" "net" "net/url" "regexp" "strconv" "strings" ) var ( controlPlaneIdentityPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`) trustDomainLabelPattern = regexp.MustCompile(`^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$`) ) 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 } } if cfg.Metrics.Enabled { if cfg.Metrics.Listen == "" { return fmt.Errorf("validate metrics listen: address is required") } if _, err := validateListenAddress("metrics", cfg.Metrics.Listen); err != nil { return err } } if err := validateControlPlane(cfg.ControlPlane); err != nil { return err } if fetchConfigured(cfg.Defaults.Fetch) { if err := validateFetch("defaults.fetch", cfg.Defaults.Fetch); err != nil { return err } } if err := validateCheck("defaults.check", cfg.Defaults.Check); err != nil { return err } if len(cfg.Upstreams) > MaximumUpstreams { return fmt.Errorf("validate configuration: upstream count exceeds %d", MaximumUpstreams) } enabledUpstreams := 0 for name, upstream := range cfg.Upstreams { if upstream.Enabled { enabledUpstreams++ } effective := upstream if upstream.Enabled { effective.Check = EffectiveCheck(cfg.Defaults.Check, upstream.Check) } if err := validateUpstream(name, effective); err != nil { return err } } if enabledUpstreams == 0 { return fmt.Errorf("validate configuration: at least one enabled upstream is required") } seen := make(map[string]struct{}, len(cfg.Routing)) targetProfilesByUpstream := make(map[string]int, len(cfg.Upstreams)) for index, route := range cfg.Routing { if err := validateRouting(index, route, cfg.Upstreams, seen); err != nil { return err } if !route.Enabled || len(route.Check.Targets) == 0 { continue } for _, upstreamID := range route.Upstreams { upstream, exists := cfg.Upstreams[upstreamID] if !exists || !upstream.Enabled { continue } targetProfilesByUpstream[upstreamID] += len(route.Check.Targets) if targetProfilesByUpstream[upstreamID] > MaximumTargetProfilesPerUpstream { return fmt.Errorf("validate upstream %q routing target profiles: supports at most %d", upstreamID, MaximumTargetProfilesPerUpstream) } } } if cfg.Distribution.Enabled { clientIdentificationMode := cfg.Distribution.ClientIdentification.Mode if clientIdentificationMode == "" { clientIdentificationMode = "sourceIP" } if err := validateEnum("distribution.clientIdentification.mode", clientIdentificationMode, "sourceIP", "authenticatedClient", "authenticatedClientOrSourceIP"); err != nil { return err } if clientIdentificationMode == "authenticatedClient" && cfg.Distribution.Auth.Mode == "none" { return fmt.Errorf("validate distribution clientIdentification.mode: authenticatedClient requires authentication") } if err := requirePositive("distribution.maxCountPerRequest", cfg.Distribution.Extraction.MaxCountPerRequest); err != nil { return err } if err := validateEnum("distribution.fulfillment", cfg.Distribution.Extraction.Fulfillment, "partial", "allOrNothing"); err != nil { return err } if err := requireNonNegative("distribution.minRemainingTTL", cfg.Distribution.Extraction.MinRemainingTTL); err != nil { return err } if err := requireNonNegative("distribution.maxHealthCheckAge", cfg.Distribution.Extraction.MaxHealthCheckAge); err != nil { return err } if err := requireNonNegative("distribution.reserveForGateway", cfg.Distribution.Extraction.ReserveForGateway); err != nil { return err } if err := requireNonNegative("distribution.idempotencyTTL", cfg.Distribution.Extraction.IdempotencyTTL); err != nil { return err } } return nil } func validateControlPlane(item ControlPlane) error { if err := validateClientTLS("gatewayTLS", item.GatewayTLS); err != nil { return err } if err := validateClientTLS("checkerTLS", item.CheckerTLS); err != nil { return err } if !item.Enabled { return nil } host, err := validateListenAddress("controlPlane", item.Listen) if err != nil { return err } if item.ProtocolVersion != 1 { return fmt.Errorf("validate controlPlane protocolVersion: must be 1") } heartbeat := item.HeartbeatInterval.Value() if heartbeat <= 0 { return fmt.Errorf("validate controlPlane heartbeatInterval: must be greater than zero") } if item.SessionTTL.Value()/3 < heartbeat { return fmt.Errorf("validate controlPlane sessionTTL: must be at least three heartbeat intervals") } if item.MaxStaleAge.Value() < heartbeat { return fmt.Errorf("validate controlPlane maxStaleAge: must not be shorter than heartbeatInterval") } if item.MaxMessageBytes <= 0 || item.MaxMessageBytes > 64<<20 { return fmt.Errorf("validate controlPlane maxMessageBytes: must be in [1, 67108864]") } if item.MaxRuntimeCounters <= 0 || item.MaxRuntimeCounters > MaximumPoolSize { return fmt.Errorf("validate controlPlane maxRuntimeCounters: must be in [1, %d]", MaximumPoolSize) } if item.MaxConcurrentStreams == 0 { return fmt.Errorf("validate controlPlane maxConcurrentStreams: must be positive") } switch item.TLS.Mode { case "disabled": if isPublicHost(host) { return fmt.Errorf("validate controlPlane tls: non-loopback listen requires mtls") } case "mtls": if item.TLS.CertFile == "" || item.TLS.KeyFile == "" || item.TLS.ClientCAFile == "" || item.TLS.TrustDomain == "" || item.TLS.Environment == "" { return fmt.Errorf("validate controlPlane tls: mtls requires certFile, keyFile, clientCAFile, trustDomain and environment") } if !validTrustDomain(item.TLS.TrustDomain) { return fmt.Errorf("validate controlPlane tls.trustDomain: must be a lowercase DNS name without port") } if !controlPlaneIdentityPattern.MatchString(item.TLS.Environment) { return fmt.Errorf("validate controlPlane tls.environment: must be one URI path segment") } default: return fmt.Errorf("validate controlPlane tls.mode: must be disabled or mtls") } return nil } func validateGatewayTLS(item GatewayTLS) error { return validateClientTLS("gatewayTLS", item) } func validateClientTLS(name string, item ClientTLS) error { configured := item.CertFile != "" || item.KeyFile != "" || item.ServerCAFile != "" if !configured { return nil } if item.CertFile == "" || item.KeyFile == "" || item.ServerCAFile == "" { return fmt.Errorf("validate controlPlane %s: certFile, keyFile and serverCAFile are required together", name) } return nil } func validTrustDomain(value string) bool { if len(value) == 0 || len(value) > 253 { return false } for _, label := range strings.Split(value, ".") { if !trustDomainLabelPattern.MatchString(label) { return false } } return true } 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) } if err := validateListenerAuth(name, listener.Auth); err != nil { return err } for _, limit := range []struct { name string value int }{ {name: "maxConcurrentConnections", value: listener.Limits.MaxConcurrentConnections}, {name: "requestsPerMinute", value: listener.Limits.RequestsPerMinute}, {name: "requestsPerMinutePerClient", value: listener.Limits.RequestsPerMinutePerClient}, } { if limit.value < 0 { return fmt.Errorf("validate %s limits.%s: must be non-negative", name, limit.name) } if int64(limit.value) > MaximumExactCounter { return fmt.Errorf("validate %s limits.%s: exceeds exact counter range", name, limit.name) } } host, err := validateListenAddress(name, listener.Listen) if err != nil { return 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) } if err := validateCIDRs(name+" allowCIDRs", listener.Access.AllowCIDRs); err != nil { return err } if err := validateCIDRs(name+" trustedProxies", listener.Access.TrustedProxies); err != nil { return err } if err := validateCIDRs(name+" destinationPolicy.denyCIDRs", listener.DestinationPolicy.DenyCIDRs); err != nil { return err } seenPorts := make(map[uint16]struct{}, len(listener.DestinationPolicy.AllowedPorts)) for _, port := range listener.DestinationPolicy.AllowedPorts { if port == 0 { return fmt.Errorf("validate %s destinationPolicy.allowedPorts: port must be greater than zero", name) } if _, exists := seenPorts[port]; exists { return fmt.Errorf("validate %s destinationPolicy.allowedPorts: duplicate port %d", name, port) } seenPorts[port] = struct{}{} } return nil } func validateListenAddress(name, address string) (string, error) { host, port, err := net.SplitHostPort(address) if err != nil { return "", fmt.Errorf("validate %s listen: %w", name, err) } if _, err = strconv.ParseUint(port, 10, 16); err != nil { return "", fmt.Errorf("validate %s listen: invalid port", name) } return host, nil } func validateRouting(index int, route Routing, upstreams map[string]Upstream, seen map[string]struct{}) error { 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{}{} scope := fmt.Sprintf("routing %q", route.Name) if len(route.Check.Targets) > 0 && !validTargetRoutingName(route.Name) { return fmt.Errorf("validate %s check.targets: routing name is not a valid target profile identifier", scope) } if err := validateEnum(scope+" purpose", route.Purpose, "gateway", "extract"); err != nil { return err } if route.Match.HostRegex != "" { if _, err := regexp.Compile(route.Match.HostRegex); err != nil { return fmt.Errorf("validate %s hostRegex: %w", scope, err) } } if route.Match.PathRegex != "" { if _, err := regexp.Compile(route.Match.PathRegex); err != nil { return fmt.Errorf("validate %s pathRegex: %w", scope, err) } } for _, upstream := range route.Upstreams { if _, ok := upstreams[upstream]; !ok { return fmt.Errorf("validate %s: upstream %q does not exist", scope, upstream) } } if err := validateStrategy(scope, route.Upstreams, route.Strategy); err != nil { return err } if err := validateEnum(scope+" onUnavailable.action", route.OnUnavailable.Action, "reject", "wait", "direct"); err != nil { return err } if route.OnUnavailable.WaitTimeout < 0 { return fmt.Errorf("validate %s onUnavailable.waitTimeout: must be non-negative", scope) } if route.OnUnavailable.Action == "wait" && route.OnUnavailable.WaitTimeout <= 0 { return fmt.Errorf("validate %s onUnavailable.waitTimeout: must be greater than zero for wait", scope) } if err := validateCheckURLs(scope+" check.targets", route.Check.Targets); err != nil { return err } return nil } func validateStrategy(scope string, upstreams []string, strategy Strategy) error { if err := validateEnum(scope+" strategy.type", strategy.Type, "sequential", "random", "roundRobin", "weighted", "leastConnections"); err != nil { return err } if strategy.Type == "sequential" { if len(upstreams) < 2 { return fmt.Errorf("validate %s strategy: sequential requires at least two upstreams", scope) } if err := requirePositive(scope+" strategy.switchAfterEmptyFetch", strategy.SwitchAfterEmptyFetch); err != nil { return err } if strategy.EndBehavior != "" { if err := validateEnum(scope+" strategy.endBehavior", strategy.EndBehavior, "stop", "loop", "stayLast"); err != nil { return err } } } if strategy.Type != "weighted" { if len(strategy.Weights) > 0 { return fmt.Errorf("validate %s strategy.weights: only weighted strategy accepts weights", scope) } return nil } if len(strategy.Weights) == 0 { return fmt.Errorf("validate %s strategy.weights: one positive weight per upstream is required", scope) } upstreamSet := make(map[string]struct{}, len(upstreams)) for _, upstream := range upstreams { upstreamSet[upstream] = struct{}{} weight, ok := strategy.Weights[upstream] if !ok { return fmt.Errorf("validate %s strategy.weights: upstream %q has no weight", scope, upstream) } if err := requirePositive(scope+" strategy weight for "+upstream, weight); err != nil { return err } } for upstream := range strategy.Weights { if _, ok := upstreamSet[upstream]; !ok { return fmt.Errorf("validate %s strategy.weights: upstream %q is not referenced", scope, upstream) } } return nil } func validateListenerAuth(listener string, auth Auth) error { switch auth.Mode { case "none": return nil case "usernamePassword": if auth.Username == "" || (auth.Password == "" && auth.PasswordFile == "") { return fmt.Errorf("validate %s auth.mode usernamePassword: username and password are required", listener) } case "apiKey": if auth.Header == "" || (auth.Token == "" && auth.TokenFile == "") { return fmt.Errorf("validate %s auth.mode apiKey: header and token are required", listener) } case "bearer": if auth.Token == "" && auth.TokenFile == "" { return fmt.Errorf("validate %s auth.mode bearer: token is required", listener) } case "ipWhitelist": if len(auth.CIDRs) == 0 { return fmt.Errorf("validate %s auth.mode ipWhitelist: cidrs are required", listener) } if err := validateCIDRs(listener+" auth", auth.CIDRs); err != nil { return err } case "any": if len(auth.Methods) == 0 { return fmt.Errorf("validate %s auth.mode any: methods are required", listener) } for index, method := range auth.Methods { if err := validateAuthMethod(listener, index, method); err != nil { return err } } default: return fmt.Errorf("validate %s auth.mode: unsupported value %q", listener, auth.Mode) } return nil } func validateAuthMethod(listener string, index int, method AuthMethod) error { switch method.Mode { case "usernamePassword": if method.Username == "" || (method.Password == "" && method.PasswordFile == "") { return fmt.Errorf("validate %s auth.methods[%d]: username and password are required", listener, index) } case "apiKey": if method.Header == "" || (method.Value == "" && method.ValueFile == "") { return fmt.Errorf("validate %s auth.methods[%d]: header and value are required", listener, index) } case "bearer": if method.Value == "" && method.ValueFile == "" { return fmt.Errorf("validate %s auth.methods[%d]: bearer value is required", listener, index) } case "ipWhitelist": if len(method.CIDRs) == 0 { return fmt.Errorf("validate %s auth.methods[%d]: cidrs are required", listener, index) } if err := validateCIDRs(listener+" auth method", method.CIDRs); err != nil { return err } default: return fmt.Errorf("validate %s auth.methods[%d]: unsupported mode %q", listener, index, method.Mode) } return nil } func validateCIDRs(name string, cidrs []string) error { for _, cidr := range cidrs { if _, _, err := net.ParseCIDR(cidr); err != nil { return fmt.Errorf("validate %s CIDR %q: %w", name, cidr, err) } } return nil } func validateUpstream(name string, upstream Upstream) error { if !upstream.Enabled { return nil } scope := fmt.Sprintf("upstream %q", name) if err := requirePositive(scope+" pool.maxSize", upstream.Pool.MaxSize); err != nil { return err } if upstream.Pool.MaxSize > MaximumPoolSize { return fmt.Errorf("validate %s pool.maxSize: exceeds %d", scope, MaximumPoolSize) } if err := requireNonNegative(scope+" fetch.maxTotal", upstream.Fetch.MaxTotal); err != nil { return err } if int64(upstream.Fetch.MaxTotal) > MaximumExactCounter { return fmt.Errorf("validate %s fetch.maxTotal: exceeds exact counter range", scope) } if upstream.Fetch.MaxTotal > 0 && upstream.Fetch.MaxTotal < upstream.Pool.MaxSize { return fmt.Errorf("validate %s fetch.maxTotal: cannot be lower than pool.maxSize", scope) } if err := requirePositive(scope+" capacity.maxConcurrencyPerProxy", upstream.Capacity.MaxConcurrencyPerProxy); err != nil { return err } if int64(upstream.Capacity.MaxConcurrencyPerProxy) > MaximumExactCounter { return fmt.Errorf("validate %s capacity.maxConcurrencyPerProxy: exceeds exact counter range", scope) } if err := requirePositive(scope+" refill.reconcileInterval", upstream.Refill.ReconcileInterval); err != nil { return err } if err := requirePositive(scope+" refill.minimumAvailableSlots", upstream.Refill.MinimumAvailableSlots); err != nil { return err } if upstream.Refill.MinimumAvailableSlots > MaximumExactCounter { return fmt.Errorf("validate %s refill.minimumAvailableSlots: exceeds exact counter range", scope) } if upstream.Refill.TargetAvailableSlots <= upstream.Refill.MinimumAvailableSlots { return fmt.Errorf("validate %s refill.targetAvailableSlots: must be greater than minimumAvailableSlots", scope) } if upstream.Refill.TargetAvailableSlots > MaximumExactCounter { return fmt.Errorf("validate %s refill.targetAvailableSlots: exceeds exact counter range", scope) } if int64(upstream.Pool.MaxSize) > math.MaxInt64/int64(upstream.Capacity.MaxConcurrencyPerProxy) { return fmt.Errorf("validate %s refill.targetAvailableSlots: theoretical capacity overflows int64", scope) } theoreticalSlots := int64(upstream.Pool.MaxSize) * int64(upstream.Capacity.MaxConcurrencyPerProxy) if theoreticalSlots > MaximumExactCounter { return fmt.Errorf("validate %s refill.targetAvailableSlots: theoretical capacity exceeds exact counter range", scope) } if upstream.Refill.TargetAvailableSlots > theoreticalSlots { return fmt.Errorf("validate %s refill.targetAvailableSlots: exceeds theoretical capacity", scope) } if err := requirePositive(scope+" lifecycle.ttl", upstream.Lifecycle.TTL); err != nil { return err } if err := requireNonNegative(scope+" lifecycle.allocationSafetyMargin", upstream.Lifecycle.AllocationSafetyMargin); err != nil { return err } if upstream.Lifecycle.TTL > 0 && upstream.Lifecycle.AllocationSafetyMargin >= upstream.Lifecycle.TTL { return fmt.Errorf("validate %s lifecycle.allocationSafetyMargin: must be lower than ttl", scope) } if err := validateFetch(scope+" fetch", upstream.Fetch); err != nil { return err } if upstream.Fetch.EstimatedIPsPerCall > upstream.Pool.MaxSize { return fmt.Errorf("validate %s fetch.estimatedIPsPerCall: cannot exceed pool.maxSize", scope) } if upstream.Fetch.MaxTotal > 0 && upstream.Fetch.EstimatedIPsPerCall > upstream.Fetch.MaxTotal { return fmt.Errorf("validate %s fetch.estimatedIPsPerCall: cannot exceed fetch.maxTotal", scope) } if err := validateCheck(scope+" check", upstream.Check); err != nil { return err } 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 validateFetch(scope string, fetch Fetch) error { if err := requirePositive(scope+".estimatedIPsPerCall", fetch.EstimatedIPsPerCall); err != nil { return err } if err := requireNonNegative(scope+".requestInterval", fetch.RequestInterval); err != nil { return err } if err := requirePositive(scope+".timeout", fetch.Timeout); err != nil { return err } if err := requirePositive(scope+".maxAttempts", fetch.MaxAttempts); err != nil { return err } if err := requirePositive(scope+".maxInFlight", fetch.MaxInFlight); err != nil { return err } if int64(fetch.MaxInFlight) > MaximumExactCounter { return fmt.Errorf("validate %s.maxInFlight: exceeds exact counter range", scope) } if err := requireNonNegative(scope+".maxTotal", fetch.MaxTotal); err != nil { return err } if err := requireNonNegative(scope+".maxResponseBytes", fetch.MaxResponseBytes); err != nil { return err } if err := requireNonNegative(scope+".templateTimeout", fetch.TemplateTimeout); err != nil { return err } if err := requirePercentage(scope+".retry.jitter", fetch.Retry.Jitter); err != nil { return err } if err := requireNonNegative(scope+".retry.initial", fetch.Retry.Initial); err != nil { return err } if err := requireNonNegative(scope+".retry.max", fetch.Retry.Max); err != nil { return err } if (fetch.Retry.Initial == 0) != (fetch.Retry.Max == 0) { return fmt.Errorf("validate %s.retry: initial and max must be configured together", scope) } if fetch.Retry.Max > 0 && fetch.Retry.Max < fetch.Retry.Initial { return fmt.Errorf("validate %s.retry.max: must not be lower than initial", scope) } return nil } func validateCheck(scope string, check Check) error { if !checkConfigured(check) { return nil } if err := requirePositive(scope+".interval", check.Interval); err != nil { return err } if err := requirePercentage(scope+".jitter", check.Jitter); err != nil { return err } if err := requirePositive(scope+".maxInFlight", check.MaxInFlight); err != nil { return err } if err := requirePositive(scope+".timeout", check.Timeout); err != nil { return err } if err := requirePositive(scope+".maxAttempts", check.MaxAttempts); err != nil { return err } if err := requirePositive(scope+".maxConsecutiveFailures", check.MaxConsecutiveFailures); err != nil { return err } if err := requireNonNegative(scope+".unhealthyRemoveAfter", check.UnhealthyRemoveAfter); err != nil { return err } return validateCheckURLs(scope+".urls", check.URLs) } func validateCheckURLs(scope string, urls []string) error { if len(urls) > MaximumCheckURLs { return fmt.Errorf("validate %s: supports at most %d URLs", scope, MaximumCheckURLs) } seenURLs := make(map[string]struct{}, len(urls)) for index, rawURL := range urls { if rawURL == "" || strings.TrimSpace(rawURL) != rawURL { return fmt.Errorf("validate %s[%d]: must be an absolute http or https URL", scope, index) } parsed, err := url.Parse(rawURL) if err != nil || parsed.Scheme == "" || parsed.Host == "" || parsed.User != nil || parsed.Fragment != "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { return fmt.Errorf("validate %s[%d]: must be an absolute http or https URL", scope, index) } canonical := parsed.String() if _, exists := seenURLs[canonical]; exists { return fmt.Errorf("validate %s[%d]: duplicate URL", scope, index) } seenURLs[canonical] = struct{}{} } return nil } func validTargetRoutingName(value string) bool { if value == "" || len(value) > 256 || strings.TrimSpace(value) != value { return false } for _, character := range value { if character <= ' ' || character == '\x7f' { return false } } return true } func fetchConfigured(fetch Fetch) bool { return fetch.EstimatedIPsPerCall != 0 || fetch.RequestInterval != 0 || fetch.Timeout != 0 || fetch.MaxAttempts != 0 || fetch.MaxInFlight != 0 || fetch.MaxTotal != 0 || fetch.MaxResponseBytes != 0 || fetch.TemplateTimeout != 0 || fetch.Retry.Initial != 0 || fetch.Retry.Max != 0 || fetch.Retry.Jitter != 0 } func checkConfigured(check Check) bool { return check.Interval != 0 || check.Jitter != 0 || check.MaxInFlight != 0 || check.Timeout != 0 || check.MaxAttempts != 0 || check.MaxConsecutiveFailures != 0 || check.UnhealthyRemoveAfter != 0 || len(check.URLs) != 0 } type validationNumber interface { ~int | ~int64 } func requirePositive[T validationNumber](field string, value T) error { if value <= 0 { return fmt.Errorf("validate %s: must be greater than zero", field) } return nil } func requireNonNegative[T validationNumber](field string, value T) error { if value < 0 { return fmt.Errorf("validate %s: must be non-negative", field) } return nil } func requirePercentage(field string, value int) error { if value < 0 || value > 100 { return fmt.Errorf("validate %s: must be between 0 and 100", field) } return nil } func validateEnum(field, value string, allowed ...string) error { for _, candidate := range allowed { if value == candidate { return nil } } return fmt.Errorf("validate %s: unsupported value %q; allowed values: %s", field, value, strings.Join(allowed, ", ")) } 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() }