package config import ( "crypto/rand" "encoding/hex" "fmt" "os" "time" "gopkg.in/yaml.v3" ) const DefaultDBPath = "./data/wormhole.db" type Config struct { Server ServerConfig `yaml:"server" json:"server"` Database DatabaseConfig `yaml:"database" json:"database"` Auth AuthConfig `yaml:"auth" json:"auth"` Metrics MetricsConfig `yaml:"metrics" json:"metrics"` } type ServerConfig struct { BridgeAddr string `yaml:"bridge_addr" json:"bridge_addr"` HTTPAddr string `yaml:"http_addr" json:"http_addr"` HTTPProxyPort int `yaml:"http_proxy_port" json:"http_proxy_port"` HTTPSProxyPort int `yaml:"https_proxy_port" json:"https_proxy_port"` TLSCertFile string `yaml:"tls_cert_file" json:"tls_cert_file"` TLSKeyFile string `yaml:"tls_key_file" json:"tls_key_file"` } type DatabaseConfig struct { Path string `yaml:"path" json:"path"` } type AuthConfig struct { JWTSecret string `yaml:"jwt_secret" json:"jwt_secret"` TokenTTL time.Duration `yaml:"token_ttl" json:"token_ttl"` LoginMaxAttempts int `yaml:"login_max_attempts" json:"login_max_attempts"` LoginWindow time.Duration `yaml:"login_window" json:"login_window"` LoginLockout time.Duration `yaml:"login_lockout" json:"login_lockout"` } type MetricsConfig struct { FlushInterval time.Duration `yaml:"flush_interval" json:"flush_interval"` } // SettingsDTO is the API/UI representation of runtime settings. type SettingsDTO struct { Server ServerConfig `json:"server"` Auth AuthSettings `json:"auth"` Metrics struct { FlushIntervalSec int `json:"flush_interval_sec"` } `json:"metrics"` } type AuthSettings struct { JWTSecret string `json:"jwt_secret,omitempty"` TokenTTLHours int `json:"token_ttl_hours"` LoginMaxAttempts int `json:"login_max_attempts"` LoginWindowMin int `json:"login_window_min"` LoginLockoutMin int `json:"login_lockout_min"` } func Default() *Config { secret, _ := randomSecret(32) return &Config{ Server: ServerConfig{ BridgeAddr: ":8528", HTTPAddr: ":8529", HTTPProxyPort: 8081, HTTPSProxyPort: 8443, }, Database: DatabaseConfig{ Path: DefaultDBPath, }, Auth: AuthConfig{ JWTSecret: secret, TokenTTL: 24 * time.Hour, LoginMaxAttempts: 5, LoginWindow: 5 * time.Minute, LoginLockout: 15 * time.Minute, }, Metrics: MetricsConfig{ FlushInterval: 30 * time.Second, }, } } func randomSecret(n int) (string, error) { b := make([]byte, n) if _, err := rand.Read(b); err != nil { return "wormhole-change-me-in-production", err } return hex.EncodeToString(b), nil } func (c *Config) Normalize() { if c.Server.BridgeAddr == "" { c.Server.BridgeAddr = ":8528" } if c.Server.HTTPAddr == "" { c.Server.HTTPAddr = ":8529" } if c.Server.HTTPProxyPort == 0 { c.Server.HTTPProxyPort = 8081 } if c.Server.HTTPSProxyPort == 0 { c.Server.HTTPSProxyPort = 8443 } if c.Database.Path == "" { c.Database.Path = DefaultDBPath } if c.Auth.TokenTTL == 0 { c.Auth.TokenTTL = 24 * time.Hour } if c.Auth.LoginMaxAttempts == 0 { c.Auth.LoginMaxAttempts = 5 } if c.Auth.LoginWindow == 0 { c.Auth.LoginWindow = 5 * time.Minute } if c.Auth.LoginLockout == 0 { c.Auth.LoginLockout = 15 * time.Minute } if c.Auth.JWTSecret == "" { secret, _ := randomSecret(32) c.Auth.JWTSecret = secret } if c.Metrics.FlushInterval == 0 { c.Metrics.FlushInterval = 30 * time.Second } } func (c *Config) ToDTO() SettingsDTO { sec := int(c.Metrics.FlushInterval / time.Second) if sec < 1 { sec = 30 } dto := SettingsDTO{ Server: c.Server, Auth: AuthSettings{ TokenTTLHours: int(c.Auth.TokenTTL / time.Hour), LoginMaxAttempts: c.Auth.LoginMaxAttempts, LoginWindowMin: int(c.Auth.LoginWindow / time.Minute), LoginLockoutMin: int(c.Auth.LoginLockout / time.Minute), }, } dto.Metrics.FlushIntervalSec = sec return dto } func SettingsFromDTO(d SettingsDTO, existing *Config) *Config { cfg := Default() if existing != nil { *cfg = *existing } cfg.Server = d.Server cfg.Auth.TokenTTL = time.Duration(d.Auth.TokenTTLHours) * time.Hour cfg.Auth.LoginMaxAttempts = d.Auth.LoginMaxAttempts cfg.Auth.LoginWindow = time.Duration(d.Auth.LoginWindowMin) * time.Minute cfg.Auth.LoginLockout = time.Duration(d.Auth.LoginLockoutMin) * time.Minute if d.Auth.JWTSecret != "" { cfg.Auth.JWTSecret = d.Auth.JWTSecret } if d.Metrics.FlushIntervalSec > 0 { cfg.Metrics.FlushInterval = time.Duration(d.Metrics.FlushIntervalSec) * time.Second } cfg.Normalize() return cfg } func (c *Config) Summary() string { return fmt.Sprintf( "http_addr=%s bridge_addr=%s http_proxy_port=%d https_proxy_port=%d db=%s", c.Server.HTTPAddr, c.Server.BridgeAddr, c.Server.HTTPProxyPort, c.Server.HTTPSProxyPort, c.Database.Path, ) } // Load reads config from a YAML file (tests only). func Load(path string) (*Config, error) { if path == "" { return nil, fmt.Errorf("config path required") } data, err := os.ReadFile(path) if err != nil { return nil, err } cfg := Default() if err := yaml.Unmarshal(data, cfg); err != nil { return nil, err } cfg.Normalize() return cfg, nil }