package config import ( "fmt" "net" "os" "strings" "go.yaml.in/yaml/v3" ) // MaxBodyBytesCap 是 max_body_bytes 的硬上限(DEVELOPMENT 11.1)。 const MaxBodyBytesCap = 262144 // Config 对应 DEVELOPMENT 第 11.1 节的配置文件。 type Config struct { Listen string `yaml:"listen"` AdminListen string `yaml:"admin_listen"` TLS TLSConfig `yaml:"tls"` TrustedProxies []string `yaml:"trusted_proxies"` DataDir string `yaml:"data_dir"` Limits LimitsConfig `yaml:"limits"` SessionIdleDays int `yaml:"session_idle_days"` RecordRetentionDays int `yaml:"record_retention_days"` IdempotencyHours int `yaml:"idempotency_hours"` ReceiptRetentionDays int `yaml:"receipt_retention_days"` SQLiteSynchronous string `yaml:"sqlite_synchronous"` Metrics MetricsConfig `yaml:"metrics"` Log LogConfig `yaml:"log"` } type TLSConfig struct { CertFile string `yaml:"cert_file"` KeyFile string `yaml:"key_file"` AllowPlaintext bool `yaml:"allow_plaintext"` } type LimitsConfig struct { MaxBodyBytes int `yaml:"max_body_bytes"` MaxMetaBytes int `yaml:"max_meta_bytes"` MaxFrameBytes int `yaml:"max_frame_bytes"` MaxTTLSeconds int `yaml:"max_ttl_seconds"` MaxScheduleSeconds int `yaml:"max_schedule_seconds"` MaxGroupMembers int `yaml:"max_group_members"` GraceSeconds int `yaml:"grace_seconds"` AckTimeoutSeconds int `yaml:"ack_timeout_seconds"` DeliveryWindow int `yaml:"delivery_window"` ReceiptWindow int `yaml:"receipt_window"` RequestsPerSecond int `yaml:"requests_per_second"` MaxPendingPerSender int `yaml:"max_pending_per_sender"` MaxPendingPerReceiver int `yaml:"max_pending_per_receiver"` } type MetricsConfig struct { Token string `yaml:"token"` } type LogConfig struct { Level string `yaml:"level"` } // Default 返回与 DEVELOPMENT 11.1 示例一致的默认值。 func Default() Config { return Config{ Listen: ":7443", AdminListen: "", TLS: TLSConfig{ AllowPlaintext: false, }, TrustedProxies: nil, DataDir: "./data", Limits: LimitsConfig{ MaxBodyBytes: MaxBodyBytesCap, MaxMetaBytes: 4096, MaxFrameBytes: 786432, MaxTTLSeconds: 2592000, MaxScheduleSeconds: 31536000, MaxGroupMembers: 1000, GraceSeconds: 60, AckTimeoutSeconds: 300, DeliveryWindow: 32, ReceiptWindow: 64, RequestsPerSecond: 50, MaxPendingPerSender: 10000, MaxPendingPerReceiver: 10000, }, SessionIdleDays: 30, RecordRetentionDays: 7, IdempotencyHours: 24, ReceiptRetentionDays: 7, SQLiteSynchronous: "FULL", Metrics: MetricsConfig{}, Log: LogConfig{ Level: "info", }, } } // Load 从路径加载 YAML 配置;空字段保留默认值。 func Load(path string) (Config, error) { cfg := Default() data, err := os.ReadFile(path) if err != nil { return Config{}, fmt.Errorf("read config: %w", err) } if err := yaml.Unmarshal(data, &cfg); err != nil { return Config{}, fmt.Errorf("parse config: %w", err) } applyEmptyDefaults(&cfg) return cfg, nil } func applyEmptyDefaults(cfg *Config) { def := Default() if cfg.Listen == "" { cfg.Listen = def.Listen } if cfg.DataDir == "" { cfg.DataDir = def.DataDir } if cfg.SQLiteSynchronous == "" { cfg.SQLiteSynchronous = def.SQLiteSynchronous } if cfg.Log.Level == "" { cfg.Log.Level = def.Log.Level } if cfg.Limits.MaxBodyBytes == 0 { cfg.Limits.MaxBodyBytes = def.Limits.MaxBodyBytes } if cfg.Limits.MaxMetaBytes == 0 { cfg.Limits.MaxMetaBytes = def.Limits.MaxMetaBytes } if cfg.Limits.MaxFrameBytes == 0 { cfg.Limits.MaxFrameBytes = def.Limits.MaxFrameBytes } if cfg.Limits.MaxTTLSeconds == 0 { cfg.Limits.MaxTTLSeconds = def.Limits.MaxTTLSeconds } if cfg.Limits.MaxScheduleSeconds == 0 { cfg.Limits.MaxScheduleSeconds = def.Limits.MaxScheduleSeconds } if cfg.Limits.MaxGroupMembers == 0 { cfg.Limits.MaxGroupMembers = def.Limits.MaxGroupMembers } if cfg.Limits.GraceSeconds == 0 { cfg.Limits.GraceSeconds = def.Limits.GraceSeconds } if cfg.Limits.AckTimeoutSeconds == 0 { cfg.Limits.AckTimeoutSeconds = def.Limits.AckTimeoutSeconds } if cfg.Limits.DeliveryWindow == 0 { cfg.Limits.DeliveryWindow = def.Limits.DeliveryWindow } if cfg.Limits.ReceiptWindow == 0 { cfg.Limits.ReceiptWindow = def.Limits.ReceiptWindow } if cfg.Limits.RequestsPerSecond == 0 { cfg.Limits.RequestsPerSecond = def.Limits.RequestsPerSecond } } // Validate 校验 DEVELOPMENT 11.1 全部字段;拒绝 max_body_bytes > 262144。 func (c Config) Validate() error { var errs []string if strings.TrimSpace(c.Listen) == "" { errs = append(errs, "listen is required") } if strings.TrimSpace(c.DataDir) == "" { errs = append(errs, "data_dir is required") } sync := strings.ToUpper(strings.TrimSpace(c.SQLiteSynchronous)) if sync != "FULL" && sync != "NORMAL" { errs = append(errs, "sqlite_synchronous must be FULL or NORMAL") } switch strings.ToLower(strings.TrimSpace(c.Log.Level)) { case "debug", "info", "warn", "warning", "error": default: errs = append(errs, "log.level must be debug, info, warn, or error") } if c.Limits.MaxBodyBytes <= 0 { errs = append(errs, "limits.max_body_bytes must be > 0") } if c.Limits.MaxBodyBytes > MaxBodyBytesCap { errs = append(errs, fmt.Sprintf("limits.max_body_bytes must be <= %d", MaxBodyBytesCap)) } if c.Limits.MaxMetaBytes <= 0 { errs = append(errs, "limits.max_meta_bytes must be > 0") } if c.Limits.MaxFrameBytes <= 0 { errs = append(errs, "limits.max_frame_bytes must be > 0") } if c.Limits.MaxFrameBytes < c.Limits.MaxBodyBytes { errs = append(errs, "limits.max_frame_bytes must be >= limits.max_body_bytes") } if c.Limits.MaxTTLSeconds <= 0 { errs = append(errs, "limits.max_ttl_seconds must be > 0") } if c.Limits.MaxScheduleSeconds <= 0 { errs = append(errs, "limits.max_schedule_seconds must be > 0") } if c.Limits.MaxGroupMembers <= 0 { errs = append(errs, "limits.max_group_members must be > 0") } if c.Limits.GraceSeconds < 0 { errs = append(errs, "limits.grace_seconds must be >= 0") } if c.Limits.AckTimeoutSeconds <= 0 { errs = append(errs, "limits.ack_timeout_seconds must be > 0") } if c.Limits.DeliveryWindow <= 0 { errs = append(errs, "limits.delivery_window must be > 0") } if c.Limits.ReceiptWindow <= 0 { errs = append(errs, "limits.receipt_window must be > 0") } if c.Limits.RequestsPerSecond <= 0 { errs = append(errs, "limits.requests_per_second must be > 0") } if c.Limits.MaxPendingPerSender < 0 { errs = append(errs, "limits.max_pending_per_sender must be >= 0") } if c.Limits.MaxPendingPerReceiver < 0 { errs = append(errs, "limits.max_pending_per_receiver must be >= 0") } if c.SessionIdleDays < 0 { errs = append(errs, "session_idle_days must be >= 0") } if c.RecordRetentionDays < 0 { errs = append(errs, "record_retention_days must be >= 0") } if c.IdempotencyHours < 0 { errs = append(errs, "idempotency_hours must be >= 0") } if c.ReceiptRetentionDays < 0 { errs = append(errs, "receipt_retention_days must be >= 0") } cert := strings.TrimSpace(c.TLS.CertFile) key := strings.TrimSpace(c.TLS.KeyFile) if (cert == "") != (key == "") { errs = append(errs, "tls.cert_file and tls.key_file must both be set or both empty") } for _, cidr := range c.TrustedProxies { if _, _, err := net.ParseCIDR(strings.TrimSpace(cidr)); err != nil { errs = append(errs, fmt.Sprintf("trusted_proxies entry %q is not a valid CIDR", cidr)) } } if len(errs) > 0 { return fmt.Errorf("invalid config: %s", strings.Join(errs, "; ")) } return nil } // PathFromEnv 返回 NIXMSG_CONFIG 或默认 ./config.yaml。 func PathFromEnv() string { if p := os.Getenv("NIXMSG_CONFIG"); p != "" { return p } return "./config.yaml" }