235 lines
7.3 KiB
Go
235 lines
7.3 KiB
Go
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"
|
||
}
|