Files
NixMsg/internal/config/config.go
T

235 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"fmt"
"net"
"os"
"strings"
"go.yaml.in/yaml/v3"
)
// MaxBodyBytesCap 是 max_body_bytes 的硬上限(DEVELOPMENT 11.1)。
const MaxBodyBytesCap = 262144
// MaxFrameBytesCap 是 max_frame_bytes 的硬上限,与 broker MaximumPacketSize 一致。
const MaxFrameBytesCap = 786432
// 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
}
// 数值字段不在这里把 0 改回默认:Load 已先填 Default 再解析 YAML,
// 未写的字段保留默认值;显式写 0 对 grace 等字段有意义,其余由 Validate 拒绝。
}
// 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 > MaxFrameBytesCap {
errs = append(errs, fmt.Sprintf("limits.max_frame_bytes must be <= %d", MaxFrameBytesCap))
}
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"
}