194 lines
6.4 KiB
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()
|
|
}
|