package config import ( "fmt" "net" "os" "strings" "github.com/docker/go-units" "gopkg.in/yaml.v3" ) type Config struct { // Server configuration ListenAddress string `yaml:"listen_address" default:":80"` // Concurrency limits MaxConcurrentRequests int64 `yaml:"max_concurrent_requests" default:"200"` MaxRequestsPerClient int64 `yaml:"max_requests_per_client" default:"5"` // P1 hardening limits (security/correctness) MaxObjectSize string `yaml:"max_object_size" default:"0"` // 0=unlimited; e.g. "256MB" protects against OOM from huge/malicious upstream responses (P1-01) TrustedProxies []string `yaml:"trusted_proxies"` // CIDR list; empty=never trust X-Forwarded-For (safe default, P1-02). See README security notes. // Cache configuration Cache CacheConfig `yaml:"cache"` // Upstream configuration Upstream string `yaml:"upstream"` } type CacheConfig struct { // Memory cache settings Memory MemoryConfig `yaml:"memory"` // Disk cache settings Disk DiskConfig `yaml:"disk"` } type MemoryConfig struct { // Size of memory cache (e.g., "512MB", "1GB") Size string `yaml:"size" default:"0"` // Garbage collection algorithm: lru, lfu, fifo, largest, smallest, hybrid GCAlgorithm string `yaml:"gc_algorithm" default:"lru"` } type DiskConfig struct { // Size of disk cache (e.g., "10GB", "50GB") Size string `yaml:"size" default:"0"` // Path to disk cache directory Path string `yaml:"path" default:""` // Garbage collection algorithm: lru, lfu, fifo, largest, smallest, hybrid GCAlgorithm string `yaml:"gc_algorithm" default:"lru"` } // LoadConfig loads configuration from a YAML file func LoadConfig(configPath string) (*Config, error) { if configPath == "" { configPath = "config.yaml" } data, err := os.ReadFile(configPath) if err != nil { return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err) } var config Config if err := yaml.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err) } // Set defaults for empty values if config.ListenAddress == "" { config.ListenAddress = ":80" } if config.MaxConcurrentRequests == 0 { config.MaxConcurrentRequests = 50 } if config.MaxRequestsPerClient == 0 { config.MaxRequestsPerClient = 3 } if config.MaxObjectSize == "" { config.MaxObjectSize = "0" } if config.TrustedProxies == nil { config.TrustedProxies = []string{} } if config.Cache.Memory.Size == "" { config.Cache.Memory.Size = "0" } if config.Cache.Memory.GCAlgorithm == "" { config.Cache.Memory.GCAlgorithm = "lru" } if config.Cache.Disk.Size == "" { config.Cache.Disk.Size = "0" } if config.Cache.Disk.GCAlgorithm == "" { config.Cache.Disk.GCAlgorithm = "lru" } return &config, nil } // SaveDefaultConfig creates a default configuration file func SaveDefaultConfig(configPath string) error { if configPath == "" { configPath = "config.yaml" } defaultConfig := Config{ ListenAddress: ":80", MaxConcurrentRequests: 50, // Reduced for home user (less concurrent load) MaxRequestsPerClient: 3, // Reduced for home user (more conservative per client) MaxObjectSize: "0", // 0=unlimited; set e.g. "512MB" for DoS protection on large bodies (P1-01) TrustedProxies: []string{}, // Conservative default: never trust XFF (P1-02 spoof prevention) Cache: CacheConfig{ Memory: MemoryConfig{ Size: "1GB", // Recommended for systems that can spare 1GB RAM for caching GCAlgorithm: "lru", }, Disk: DiskConfig{ Size: "1TB", // Large HDD cache for home user Path: "./disk", GCAlgorithm: "lru", // Better for gaming patterns (keeps recently played games) }, }, Upstream: "", } data, err := yaml.Marshal(&defaultConfig) if err != nil { return fmt.Errorf("failed to marshal default config: %w", err) } if err := os.WriteFile(configPath, data, 0644); err != nil { return fmt.Errorf("failed to write default config file: %w", err) } return nil } // GetDefaultConfig returns a populated default configuration (for tests and convenience). func GetDefaultConfig() Config { return Config{ ListenAddress: ":80", MaxConcurrentRequests: 50, MaxRequestsPerClient: 3, MaxObjectSize: "0", // 0=unlimited (override for bounded response safety) TrustedProxies: []string{}, // safe default: do not trust forwarded headers Cache: CacheConfig{ Memory: MemoryConfig{ Size: "1GB", GCAlgorithm: "lru", }, Disk: DiskConfig{ Size: "1TB", Path: "./disk", GCAlgorithm: "lru", }, }, Upstream: "", } } // Validate performs basic sanity checks on the configuration. func (c Config) Validate() error { if c.MaxConcurrentRequests < 0 { return fmt.Errorf("negative concurrency not allowed") } if c.MaxRequestsPerClient < 0 { return fmt.Errorf("negative per-client limit not allowed") } if c.Cache.Memory.GCAlgorithm != "" { switch c.Cache.Memory.GCAlgorithm { case "lru", "lfu", "fifo", "largest", "smallest", "hybrid": default: return fmt.Errorf("invalid memory gc algorithm: %s", c.Cache.Memory.GCAlgorithm) } } if c.Cache.Disk.Size != "" && c.Cache.Disk.Size != "0" && c.Cache.Disk.Path == "" { return fmt.Errorf("disk cache enabled but no path specified") } // P1 light validation for security/resource fields (mirrors existing GC + path checks; fails fast before New) if c.MaxObjectSize != "" && c.MaxObjectSize != "0" { if _, err := units.FromHumanSize(c.MaxObjectSize); err != nil { return fmt.Errorf("invalid max_object_size: %w", err) } } for _, p := range c.TrustedProxies { p = strings.TrimSpace(p) if p == "" { continue } if !strings.Contains(p, "/") { if net.ParseIP(p) == nil { return fmt.Errorf("invalid trusted_proxies entry (not IP or CIDR): %s", p) } continue } if _, _, err := net.ParseCIDR(p); err != nil { return fmt.Errorf("invalid trusted_proxies CIDR: %s", p) } } if c.MaxConcurrentRequests < 0 || c.MaxRequestsPerClient < 0 { // already covered above but explicit for P1 knobs // covered by earlier checks } return nil }