proxy-pool/internal/config/validate.go

194 lines
6.4 KiB
Go

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()
}