package main import ( "fmt" "os" "strconv" "strings" "time" "github.com/BurntSushi/toml" ) const defaultPort = "8066" const defaultRequestTimeoutSec = 60 type Route struct { Prefixes []string URL string } type Config struct { Port string Routes []Route RequestTimeout time.Duration } type fileConfig struct { Listen string `toml:"listen"` RequestTimeoutSec int `toml:"request_timeout_sec"` Routes []routeConfig `toml:"routes"` } type routeConfig struct { Prefixes []string `toml:"prefixes"` URL string `toml:"url"` } func LoadConfig() (Config, error) { configFile := os.Getenv("CONFIG_FILE") if configFile == "" { return Config{}, fmt.Errorf("CONFIG_FILE is required") } data, err := os.ReadFile(configFile) if err != nil { return Config{}, fmt.Errorf("read config file: %w", err) } var fc fileConfig if err := toml.Unmarshal(data, &fc); err != nil { return Config{}, fmt.Errorf("invalid config file: %w", err) } cfg := Config{ Port: envOr("PORT", listenPort(fc.Listen)), RequestTimeout: time.Duration(envIntOr("REQUEST_TIMEOUT_SEC", timeoutSec(fc.RequestTimeoutSec))) * time.Second, } cfg.Routes, err = normalizeRoutes(fc.Routes) if err != nil { return Config{}, err } return cfg, nil } func listenPort(listen string) string { listen = strings.TrimSpace(listen) if listen == "" { return defaultPort } if strings.HasPrefix(listen, ":") { return strings.TrimPrefix(listen, ":") } if idx := strings.LastIndex(listen, ":"); idx >= 0 && idx < len(listen)-1 { return listen[idx+1:] } return listen } func timeoutSec(sec int) int { if sec <= 0 { return defaultRequestTimeoutSec } return sec } func normalizeRoutes(raw []routeConfig) ([]Route, error) { if len(raw) == 0 { return nil, fmt.Errorf("at least one route is required") } routes := make([]Route, 0, len(raw)) seen := make(map[string]string) for i, r := range raw { url := strings.TrimRight(strings.TrimSpace(r.URL), "/") if url == "" { return nil, fmt.Errorf("route %d: url is required", i) } prefixes := make([]string, 0, len(r.Prefixes)) for _, prefix := range r.Prefixes { prefix = strings.TrimSpace(prefix) if prefix == "" { continue } if otherURL, ok := seen[prefix]; ok { return nil, fmt.Errorf("duplicate prefix %q in routes for %q and %q", prefix, otherURL, url) } seen[prefix] = url prefixes = append(prefixes, prefix) } if len(prefixes) == 0 { return nil, fmt.Errorf("route %d: at least one prefix is required", i) } routes = append(routes, Route{Prefixes: prefixes, URL: url}) } return routes, nil } func (c Config) BackendForPlatform(platform string) (string, bool) { platform = stripVersionSuffix(platform) if platform == "" { return "", false } for _, route := range c.Routes { for _, prefix := range route.Prefixes { if platform == prefix { return route.URL, true } } } return "", false } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func envIntOr(key string, fallback int) int { v := os.Getenv(key) if v == "" { return fallback } n, err := strconv.Atoi(v) if err != nil { return fallback } return n }