216 lines
6.1 KiB
Go
216 lines
6.1 KiB
Go
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
|
|
}
|