This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var aliasRe = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]*$`)
|
||||
|
||||
// Config is the gateway YAML configuration.
|
||||
type Config struct {
|
||||
Listen string `yaml:"listen"`
|
||||
AllowAll bool `yaml:"allow_all"`
|
||||
WhitelistCIDRs []string `yaml:"whitelist_cidrs"`
|
||||
TrustedProxies []string `yaml:"trusted_proxies"`
|
||||
Servers []Server `yaml:"servers"`
|
||||
}
|
||||
|
||||
// Server maps a URL alias to an upstream base URL.
|
||||
type Server struct {
|
||||
Alias string `yaml:"alias"`
|
||||
BaseURL string `yaml:"base_url"`
|
||||
PathPrefix string `yaml:"path_prefix"`
|
||||
AuthorizationEnv string `yaml:"authorization_env"`
|
||||
}
|
||||
|
||||
// Load reads and validates configuration from path.
|
||||
func Load(path string) (*Config, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read config: %w", err)
|
||||
}
|
||||
var c Config
|
||||
if err := yaml.Unmarshal(raw, &c); err != nil {
|
||||
return nil, fmt.Errorf("parse yaml: %w", err)
|
||||
}
|
||||
if err := c.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// Validate checks required fields and formats.
|
||||
func (c *Config) Validate() error {
|
||||
if c.Listen == "" {
|
||||
c.Listen = ":8080"
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
for i := range c.Servers {
|
||||
s := &c.Servers[i]
|
||||
if s.Alias == "" {
|
||||
return fmt.Errorf("servers[%d]: alias is required", i)
|
||||
}
|
||||
if !aliasRe.MatchString(s.Alias) {
|
||||
return fmt.Errorf("servers[%d]: alias %q must match %s", i, s.Alias, aliasRe.String())
|
||||
}
|
||||
if _, ok := seen[s.Alias]; ok {
|
||||
return fmt.Errorf("duplicate alias %q", s.Alias)
|
||||
}
|
||||
seen[s.Alias] = struct{}{}
|
||||
if s.BaseURL == "" {
|
||||
return fmt.Errorf("servers[%d]: base_url is required", i)
|
||||
}
|
||||
u, err := url.Parse(s.BaseURL)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return fmt.Errorf("servers[%d]: invalid base_url %q", i, s.BaseURL)
|
||||
}
|
||||
if s.PathPrefix == "" {
|
||||
s.PathPrefix = "/v1"
|
||||
}
|
||||
s.PathPrefix = strings.TrimSuffix(s.PathPrefix, "/")
|
||||
if !strings.HasPrefix(s.PathPrefix, "/") {
|
||||
s.PathPrefix = "/" + s.PathPrefix
|
||||
}
|
||||
}
|
||||
if len(c.Servers) == 0 {
|
||||
return fmt.Errorf("at least one server entry is required")
|
||||
}
|
||||
for i, s := range c.WhitelistCIDRs {
|
||||
if _, err := netip.ParsePrefix(strings.TrimSpace(s)); err != nil {
|
||||
return fmt.Errorf("whitelist_cidrs[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
for i, s := range c.TrustedProxies {
|
||||
if _, err := netip.ParsePrefix(strings.TrimSpace(s)); err != nil {
|
||||
return fmt.Errorf("trusted_proxies[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parsed holds compiled CIDR lists and server map.
|
||||
type Parsed struct {
|
||||
Config *Config
|
||||
Whitelist []netip.Prefix
|
||||
Trusted []netip.Prefix
|
||||
ByAlias map[string]*Server
|
||||
AuthByAlias map[string]string // non-empty Authorization value per alias
|
||||
}
|
||||
|
||||
// Parse compiles CIDRs and resolves authorization from environment.
|
||||
func (c *Config) Parse() (*Parsed, error) {
|
||||
var wl []netip.Prefix
|
||||
for _, s := range c.WhitelistCIDRs {
|
||||
p, err := netip.ParsePrefix(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wl = append(wl, p)
|
||||
}
|
||||
var tr []netip.Prefix
|
||||
for _, s := range c.TrustedProxies {
|
||||
p, err := netip.ParsePrefix(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr = append(tr, p)
|
||||
}
|
||||
by := make(map[string]*Server, len(c.Servers))
|
||||
auth := make(map[string]string)
|
||||
for i := range c.Servers {
|
||||
s := &c.Servers[i]
|
||||
by[s.Alias] = s
|
||||
if s.AuthorizationEnv != "" {
|
||||
v := os.Getenv(s.AuthorizationEnv)
|
||||
if v == "" {
|
||||
return nil, fmt.Errorf("server %q: env %q is empty or unset", s.Alias, s.AuthorizationEnv)
|
||||
}
|
||||
auth[s.Alias] = v
|
||||
}
|
||||
}
|
||||
return &Parsed{
|
||||
Config: c,
|
||||
Whitelist: wl,
|
||||
Trusted: tr,
|
||||
ByAlias: by,
|
||||
AuthByAlias: auth,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadExample(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "cfg.yaml")
|
||||
if err := os.WriteFile(p, []byte(`
|
||||
listen: ":0"
|
||||
allow_all: true
|
||||
servers:
|
||||
- alias: main_srv
|
||||
base_url: http://127.0.0.1:9091
|
||||
`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := Load(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.Listen != ":0" {
|
||||
t.Fatalf("listen: %q", c.Listen)
|
||||
}
|
||||
_, err = c.Parse()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDuplicateAlias(t *testing.T) {
|
||||
c := &Config{
|
||||
Servers: []Server{
|
||||
{Alias: "a", BaseURL: "http://x:1"},
|
||||
{Alias: "a", BaseURL: "http://y:2"},
|
||||
},
|
||||
}
|
||||
if err := c.Validate(); err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user