diff options
Diffstat (limited to 'internal/config/config.go')
| -rw-r--r-- | internal/config/config.go | 324 |
1 files changed, 324 insertions, 0 deletions
diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..87ca4eb --- /dev/null +++ b/internal/config/config.go | |||
| @@ -0,0 +1,324 @@ | |||
| 1 | package config | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "fmt" | ||
| 5 | "os" | ||
| 6 | "strings" | ||
| 7 | "time" | ||
| 8 | |||
| 9 | "gopkg.in/yaml.v3" | ||
| 10 | ) | ||
| 11 | |||
| 12 | // Config holds all configuration for the relay. | ||
| 13 | type Config struct { | ||
| 14 | Server ServerConfig `yaml:"server"` | ||
| 15 | Database DatabaseConfig `yaml:"database"` | ||
| 16 | Auth AuthConfig `yaml:"auth"` | ||
| 17 | RateLimit RateLimitConfig `yaml:"rate_limit"` | ||
| 18 | Metrics MetricsConfig `yaml:"metrics"` | ||
| 19 | Logging LoggingConfig `yaml:"logging"` | ||
| 20 | Storage StorageConfig `yaml:"storage"` | ||
| 21 | } | ||
| 22 | |||
| 23 | // ServerConfig holds server configuration. | ||
| 24 | type ServerConfig struct { | ||
| 25 | GrpcAddr string `yaml:"grpc_addr"` | ||
| 26 | HttpAddr string `yaml:"http_addr"` | ||
| 27 | PublicURL string `yaml:"public_url"` | ||
| 28 | ReadTimeout time.Duration `yaml:"read_timeout"` | ||
| 29 | WriteTimeout time.Duration `yaml:"write_timeout"` | ||
| 30 | } | ||
| 31 | |||
| 32 | // DatabaseConfig holds database configuration. | ||
| 33 | type DatabaseConfig struct { | ||
| 34 | Path string `yaml:"path"` | ||
| 35 | MaxConnections int `yaml:"max_connections"` | ||
| 36 | MaxLifetime time.Duration `yaml:"max_lifetime"` | ||
| 37 | } | ||
| 38 | |||
| 39 | // AuthConfig holds authentication configuration. | ||
| 40 | type AuthConfig struct { | ||
| 41 | Enabled bool `yaml:"enabled"` | ||
| 42 | Required bool `yaml:"required"` | ||
| 43 | TimestampWindow int64 `yaml:"timestamp_window"` | ||
| 44 | AllowedPubkeys []string `yaml:"allowed_pubkeys"` | ||
| 45 | SkipMethods []string `yaml:"skip_methods"` | ||
| 46 | } | ||
| 47 | |||
| 48 | // RateLimitConfig holds rate limiting configuration. | ||
| 49 | type RateLimitConfig struct { | ||
| 50 | Enabled bool `yaml:"enabled"` | ||
| 51 | DefaultRPS float64 `yaml:"default_rps"` | ||
| 52 | DefaultBurst int `yaml:"default_burst"` | ||
| 53 | IPRPS float64 `yaml:"ip_rps"` | ||
| 54 | IPBurst int `yaml:"ip_burst"` | ||
| 55 | Methods map[string]MethodLimit `yaml:"methods"` | ||
| 56 | Users map[string]UserLimit `yaml:"users"` | ||
| 57 | SkipMethods []string `yaml:"skip_methods"` | ||
| 58 | SkipUsers []string `yaml:"skip_users"` | ||
| 59 | CleanupInterval time.Duration `yaml:"cleanup_interval"` | ||
| 60 | MaxIdleTime time.Duration `yaml:"max_idle_time"` | ||
| 61 | } | ||
| 62 | |||
| 63 | // MethodLimit defines rate limits for a specific method. | ||
| 64 | type MethodLimit struct { | ||
| 65 | RPS float64 `yaml:"rps"` | ||
| 66 | Burst int `yaml:"burst"` | ||
| 67 | } | ||
| 68 | |||
| 69 | // UserLimit defines rate limits for a specific user. | ||
| 70 | type UserLimit struct { | ||
| 71 | RPS float64 `yaml:"rps"` | ||
| 72 | Burst int `yaml:"burst"` | ||
| 73 | Methods map[string]MethodLimit `yaml:"methods"` | ||
| 74 | } | ||
| 75 | |||
| 76 | // MetricsConfig holds metrics configuration. | ||
| 77 | type MetricsConfig struct { | ||
| 78 | Enabled bool `yaml:"enabled"` | ||
| 79 | Addr string `yaml:"addr"` | ||
| 80 | Path string `yaml:"path"` | ||
| 81 | Namespace string `yaml:"namespace"` | ||
| 82 | Subsystem string `yaml:"subsystem"` | ||
| 83 | } | ||
| 84 | |||
| 85 | // LoggingConfig holds logging configuration. | ||
| 86 | type LoggingConfig struct { | ||
| 87 | Level string `yaml:"level"` | ||
| 88 | Format string `yaml:"format"` | ||
| 89 | Output string `yaml:"output"` | ||
| 90 | } | ||
| 91 | |||
| 92 | // StorageConfig holds storage configuration. | ||
| 93 | type StorageConfig struct { | ||
| 94 | AutoCompact bool `yaml:"auto_compact"` | ||
| 95 | CompactInterval time.Duration `yaml:"compact_interval"` | ||
| 96 | MaxEventAge time.Duration `yaml:"max_event_age"` | ||
| 97 | } | ||
| 98 | |||
| 99 | // Default returns the default configuration. | ||
| 100 | func Default() *Config { | ||
| 101 | return &Config{ | ||
| 102 | Server: ServerConfig{ | ||
| 103 | GrpcAddr: ":50051", | ||
| 104 | HttpAddr: ":8080", | ||
| 105 | ReadTimeout: 30 * time.Second, | ||
| 106 | WriteTimeout: 30 * time.Second, | ||
| 107 | }, | ||
| 108 | Database: DatabaseConfig{ | ||
| 109 | Path: "relay.db", | ||
| 110 | MaxConnections: 10, | ||
| 111 | MaxLifetime: 1 * time.Hour, | ||
| 112 | }, | ||
| 113 | Auth: AuthConfig{ | ||
| 114 | Enabled: false, | ||
| 115 | Required: false, | ||
| 116 | TimestampWindow: 60, | ||
| 117 | }, | ||
| 118 | RateLimit: RateLimitConfig{ | ||
| 119 | Enabled: false, | ||
| 120 | DefaultRPS: 10, | ||
| 121 | DefaultBurst: 20, | ||
| 122 | IPRPS: 5, | ||
| 123 | IPBurst: 10, | ||
| 124 | CleanupInterval: 5 * time.Minute, | ||
| 125 | MaxIdleTime: 10 * time.Minute, | ||
| 126 | }, | ||
| 127 | Metrics: MetricsConfig{ | ||
| 128 | Enabled: true, | ||
| 129 | Addr: ":9090", | ||
| 130 | Path: "/metrics", | ||
| 131 | Namespace: "muxstr", | ||
| 132 | Subsystem: "relay", | ||
| 133 | }, | ||
| 134 | Logging: LoggingConfig{ | ||
| 135 | Level: "info", | ||
| 136 | Format: "json", | ||
| 137 | Output: "stdout", | ||
| 138 | }, | ||
| 139 | Storage: StorageConfig{ | ||
| 140 | AutoCompact: true, | ||
| 141 | CompactInterval: 24 * time.Hour, | ||
| 142 | MaxEventAge: 0, // unlimited | ||
| 143 | }, | ||
| 144 | } | ||
| 145 | } | ||
| 146 | |||
| 147 | // Load loads configuration from a YAML file and applies environment variable overrides. | ||
| 148 | func Load(filename string) (*Config, error) { | ||
| 149 | // Start with defaults | ||
| 150 | cfg := Default() | ||
| 151 | |||
| 152 | // Read file if provided | ||
| 153 | if filename != "" { | ||
| 154 | data, err := os.ReadFile(filename) | ||
| 155 | if err != nil { | ||
| 156 | return nil, fmt.Errorf("failed to read config file: %w", err) | ||
| 157 | } | ||
| 158 | |||
| 159 | if err := yaml.Unmarshal(data, cfg); err != nil { | ||
| 160 | return nil, fmt.Errorf("failed to parse config file: %w", err) | ||
| 161 | } | ||
| 162 | } | ||
| 163 | |||
| 164 | // Apply environment variable overrides | ||
| 165 | applyEnvOverrides(cfg) | ||
| 166 | |||
| 167 | // Validate | ||
| 168 | if err := cfg.Validate(); err != nil { | ||
| 169 | return nil, fmt.Errorf("invalid configuration: %w", err) | ||
| 170 | } | ||
| 171 | |||
| 172 | return cfg, nil | ||
| 173 | } | ||
| 174 | |||
| 175 | // Validate validates the configuration. | ||
| 176 | func (c *Config) Validate() error { | ||
| 177 | // Validate server addresses | ||
| 178 | if c.Server.GrpcAddr == "" { | ||
| 179 | return fmt.Errorf("server.grpc_addr is required") | ||
| 180 | } | ||
| 181 | if c.Server.HttpAddr == "" { | ||
| 182 | return fmt.Errorf("server.http_addr is required") | ||
| 183 | } | ||
| 184 | |||
| 185 | // Validate database path | ||
| 186 | if c.Database.Path == "" { | ||
| 187 | return fmt.Errorf("database.path is required") | ||
| 188 | } | ||
| 189 | |||
| 190 | // Validate metrics config if enabled | ||
| 191 | if c.Metrics.Enabled { | ||
| 192 | if c.Metrics.Addr == "" { | ||
| 193 | return fmt.Errorf("metrics.addr is required when metrics enabled") | ||
| 194 | } | ||
| 195 | if c.Metrics.Namespace == "" { | ||
| 196 | return fmt.Errorf("metrics.namespace is required when metrics enabled") | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | // Validate logging | ||
| 201 | validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true} | ||
| 202 | if !validLevels[c.Logging.Level] { | ||
| 203 | return fmt.Errorf("invalid logging.level: %s (must be debug, info, warn, or error)", c.Logging.Level) | ||
| 204 | } | ||
| 205 | |||
| 206 | validFormats := map[string]bool{"json": true, "text": true} | ||
| 207 | if !validFormats[c.Logging.Format] { | ||
| 208 | return fmt.Errorf("invalid logging.format: %s (must be json or text)", c.Logging.Format) | ||
| 209 | } | ||
| 210 | |||
| 211 | return nil | ||
| 212 | } | ||
| 213 | |||
| 214 | // applyEnvOverrides applies environment variable overrides to the configuration. | ||
| 215 | // Environment variables follow the pattern: MUXSTR_<SECTION>_<KEY> | ||
| 216 | func applyEnvOverrides(cfg *Config) { | ||
| 217 | // Server | ||
| 218 | if val := os.Getenv("MUXSTR_SERVER_GRPC_ADDR"); val != "" { | ||
| 219 | cfg.Server.GrpcAddr = val | ||
| 220 | } | ||
| 221 | if val := os.Getenv("MUXSTR_SERVER_HTTP_ADDR"); val != "" { | ||
| 222 | cfg.Server.HttpAddr = val | ||
| 223 | } | ||
| 224 | if val := os.Getenv("MUXSTR_SERVER_PUBLIC_URL"); val != "" { | ||
| 225 | cfg.Server.PublicURL = val | ||
| 226 | } | ||
| 227 | if val := os.Getenv("MUXSTR_SERVER_READ_TIMEOUT"); val != "" { | ||
| 228 | if d, err := time.ParseDuration(val); err == nil { | ||
| 229 | cfg.Server.ReadTimeout = d | ||
| 230 | } | ||
| 231 | } | ||
| 232 | if val := os.Getenv("MUXSTR_SERVER_WRITE_TIMEOUT"); val != "" { | ||
| 233 | if d, err := time.ParseDuration(val); err == nil { | ||
| 234 | cfg.Server.WriteTimeout = d | ||
| 235 | } | ||
| 236 | } | ||
| 237 | |||
| 238 | // Database | ||
| 239 | if val := os.Getenv("MUXSTR_DATABASE_PATH"); val != "" { | ||
| 240 | cfg.Database.Path = val | ||
| 241 | } | ||
| 242 | if val := os.Getenv("MUXSTR_DATABASE_MAX_CONNECTIONS"); val != "" { | ||
| 243 | var n int | ||
| 244 | if _, err := fmt.Sscanf(val, "%d", &n); err == nil { | ||
| 245 | cfg.Database.MaxConnections = n | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | // Auth | ||
| 250 | if val := os.Getenv("MUXSTR_AUTH_ENABLED"); val != "" { | ||
| 251 | cfg.Auth.Enabled = parseBool(val) | ||
| 252 | } | ||
| 253 | if val := os.Getenv("MUXSTR_AUTH_REQUIRED"); val != "" { | ||
| 254 | cfg.Auth.Required = parseBool(val) | ||
| 255 | } | ||
| 256 | if val := os.Getenv("MUXSTR_AUTH_TIMESTAMP_WINDOW"); val != "" { | ||
| 257 | var n int64 | ||
| 258 | if _, err := fmt.Sscanf(val, "%d", &n); err == nil { | ||
| 259 | cfg.Auth.TimestampWindow = n | ||
| 260 | } | ||
| 261 | } | ||
| 262 | if val := os.Getenv("MUXSTR_AUTH_ALLOWED_PUBKEYS"); val != "" { | ||
| 263 | cfg.Auth.AllowedPubkeys = strings.Split(val, ",") | ||
| 264 | } | ||
| 265 | |||
| 266 | // Rate limit | ||
| 267 | if val := os.Getenv("MUXSTR_RATE_LIMIT_ENABLED"); val != "" { | ||
| 268 | cfg.RateLimit.Enabled = parseBool(val) | ||
| 269 | } | ||
| 270 | if val := os.Getenv("MUXSTR_RATE_LIMIT_DEFAULT_RPS"); val != "" { | ||
| 271 | var n float64 | ||
| 272 | if _, err := fmt.Sscanf(val, "%f", &n); err == nil { | ||
| 273 | cfg.RateLimit.DefaultRPS = n | ||
| 274 | } | ||
| 275 | } | ||
| 276 | if val := os.Getenv("MUXSTR_RATE_LIMIT_DEFAULT_BURST"); val != "" { | ||
| 277 | var n int | ||
| 278 | if _, err := fmt.Sscanf(val, "%d", &n); err == nil { | ||
| 279 | cfg.RateLimit.DefaultBurst = n | ||
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | // Metrics | ||
| 284 | if val := os.Getenv("MUXSTR_METRICS_ENABLED"); val != "" { | ||
| 285 | cfg.Metrics.Enabled = parseBool(val) | ||
| 286 | } | ||
| 287 | if val := os.Getenv("MUXSTR_METRICS_ADDR"); val != "" { | ||
| 288 | cfg.Metrics.Addr = val | ||
| 289 | } | ||
| 290 | if val := os.Getenv("MUXSTR_METRICS_PATH"); val != "" { | ||
| 291 | cfg.Metrics.Path = val | ||
| 292 | } | ||
| 293 | |||
| 294 | // Logging | ||
| 295 | if val := os.Getenv("MUXSTR_LOGGING_LEVEL"); val != "" { | ||
| 296 | cfg.Logging.Level = val | ||
| 297 | } | ||
| 298 | if val := os.Getenv("MUXSTR_LOGGING_FORMAT"); val != "" { | ||
| 299 | cfg.Logging.Format = val | ||
| 300 | } | ||
| 301 | if val := os.Getenv("MUXSTR_LOGGING_OUTPUT"); val != "" { | ||
| 302 | cfg.Logging.Output = val | ||
| 303 | } | ||
| 304 | } | ||
| 305 | |||
| 306 | // parseBool parses a boolean from a string. | ||
| 307 | func parseBool(s string) bool { | ||
| 308 | s = strings.ToLower(s) | ||
| 309 | return s == "true" || s == "1" || s == "yes" || s == "on" | ||
| 310 | } | ||
| 311 | |||
| 312 | // Save saves the configuration to a YAML file. | ||
| 313 | func (c *Config) Save(filename string) error { | ||
| 314 | data, err := yaml.Marshal(c) | ||
| 315 | if err != nil { | ||
| 316 | return fmt.Errorf("failed to marshal config: %w", err) | ||
| 317 | } | ||
| 318 | |||
| 319 | if err := os.WriteFile(filename, data, 0644); err != nil { | ||
| 320 | return fmt.Errorf("failed to write config file: %w", err) | ||
| 321 | } | ||
| 322 | |||
| 323 | return nil | ||
| 324 | } | ||
