package server import ( "os" "time" "git.n1ko.dev/Niko/niko_trust/internal/tce" "gopkg.in/yaml.v3" ) // Config holds every operator-tunable knob for the relay. All fields have sane // defaults (see DefaultConfig); a YAML file overrides only the keys it sets. type Config struct { ListenAddr string // HTTP listen address, e.g. ":8080" Audience string // audience bound into auth assertions DataDir string // directory to persist objects ("" = in-memory) LogLevel string // debug | info | warn | error // Per-IP fixed-window rate limits. PutLimit int // POST /v1/objects requests per window PutWindow time.Duration // window for PutLimit ChallengeLimit int // POST /v1/auth/challenge requests per window ChallengeWindow time.Duration // window for ChallengeLimit MaxPerSubject int // max claims stored per subject address ChallengeTTL time.Duration // validity of an issued auth challenge SessionTTL time.Duration // validity of a verified auth session MaxBodyBytes int64 // hard cap on any request body } // configFile mirrors Config but keeps durations as strings so they can be // written as "5m" / "1m" in YAML (yaml.v3 does not parse time.Duration). type configFile struct { ListenAddr string `yaml:"listen_addr"` Audience string `yaml:"audience"` DataDir string `yaml:"data_dir"` LogLevel string `yaml:"log_level"` PutLimit int `yaml:"put_limit"` PutWindow string `yaml:"put_window"` ChallengeLimit int `yaml:"challenge_limit"` ChallengeWindow string `yaml:"challenge_window"` MaxPerSubject int `yaml:"max_per_subject"` ChallengeTTL string `yaml:"challenge_ttl"` SessionTTL string `yaml:"session_ttl"` MaxBodyBytes int64 `yaml:"max_body_bytes"` } // DefaultConfig returns the built-in defaults. func DefaultConfig() Config { return Config{ ListenAddr: ":8080", Audience: "trust.n1ko.dev", DataDir: "data", // persist objects; "" would be in-memory only LogLevel: "info", PutLimit: 60, PutWindow: time.Minute, ChallengeLimit: 30, ChallengeWindow: time.Minute, MaxPerSubject: 1000, ChallengeTTL: 5 * time.Minute, SessionTTL: 30 * time.Minute, MaxBodyBytes: tce.MaxClaimTCE*2 + 1024, } } // LoadConfig reads a YAML config file. A missing file yields the defaults // (so the server can run config-less); any other read or parse error is // returned. func LoadConfig(path string) (Config, error) { cfg := DefaultConfig() if path == "" { return cfg, nil } b, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { return cfg, nil } return cfg, err } return ParseConfig(b) } // ParseConfig decodes YAML bytes into a Config, layering overrides on top of // the defaults. func ParseConfig(b []byte) (Config, error) { cfg := DefaultConfig() var f configFile if err := yaml.Unmarshal(b, &f); err != nil { return cfg, err } applyConfigFile(&cfg, &f) return cfg, nil } func applyConfigFile(cfg *Config, f *configFile) { if f.ListenAddr != "" { cfg.ListenAddr = f.ListenAddr } if f.Audience != "" { cfg.Audience = f.Audience } if f.DataDir != "" { cfg.DataDir = f.DataDir } if f.LogLevel != "" { cfg.LogLevel = f.LogLevel } if f.PutLimit != 0 { cfg.PutLimit = f.PutLimit } cfg.PutWindow = parseDur(f.PutWindow, cfg.PutWindow) if f.ChallengeLimit != 0 { cfg.ChallengeLimit = f.ChallengeLimit } cfg.ChallengeWindow = parseDur(f.ChallengeWindow, cfg.ChallengeWindow) if f.MaxPerSubject != 0 { cfg.MaxPerSubject = f.MaxPerSubject } cfg.ChallengeTTL = parseDur(f.ChallengeTTL, cfg.ChallengeTTL) cfg.SessionTTL = parseDur(f.SessionTTL, cfg.SessionTTL) if f.MaxBodyBytes != 0 { cfg.MaxBodyBytes = f.MaxBodyBytes } } func parseDur(s string, def time.Duration) time.Duration { if s == "" { return def } if d, err := time.ParseDuration(s); err == nil { return d } return def }