114 lines
2.8 KiB
Go
114 lines
2.8 KiB
Go
// Package config loads and validates the Sense process configuration.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
defaultHTTPAddress = "127.0.0.1:8080"
|
|
defaultDatabaseDSN = "file:data/sense.db"
|
|
defaultMediaMTXURL = "http://127.0.0.1:9997"
|
|
defaultReconcilePeriod = 5 * time.Second
|
|
defaultProbePeriod = 10 * time.Second
|
|
)
|
|
|
|
type Config struct {
|
|
HTTPAddress string
|
|
AllowNonLoopback bool
|
|
DatabaseDSN string
|
|
MediaMTXURL string
|
|
ReconcileInterval time.Duration
|
|
ProbeInterval time.Duration
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
allow, err := boolEnv("SENSE_ALLOW_NON_LOOPBACK", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
reconcilePeriod, err := durationEnv("SENSE_RECONCILE_INTERVAL", defaultReconcilePeriod)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
probePeriod, err := durationEnv("SENSE_PROBE_INTERVAL", defaultProbePeriod)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
cfg := Config{
|
|
HTTPAddress: stringEnv("SENSE_HTTP_ADDR", defaultHTTPAddress),
|
|
AllowNonLoopback: allow,
|
|
DatabaseDSN: stringEnv("SENSE_DB_DSN", defaultDatabaseDSN),
|
|
MediaMTXURL: stringEnv("SENSE_MEDIAMTX_URL", defaultMediaMTXURL),
|
|
ReconcileInterval: reconcilePeriod,
|
|
ProbeInterval: probePeriod,
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c Config) Validate() error {
|
|
host, _, err := net.SplitHostPort(c.HTTPAddress)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid SENSE_HTTP_ADDR: %w", err)
|
|
}
|
|
ip := net.ParseIP(host)
|
|
isLoopback := host == "localhost" || (ip != nil && ip.IsLoopback())
|
|
if !isLoopback && !c.AllowNonLoopback {
|
|
return fmt.Errorf("non-loopback HTTP bind requires SENSE_ALLOW_NON_LOOPBACK=true")
|
|
}
|
|
if c.DatabaseDSN == "" {
|
|
return fmt.Errorf("SENSE_DB_DSN must not be empty")
|
|
}
|
|
mediaURL, err := url.Parse(c.MediaMTXURL)
|
|
if err != nil || mediaURL.Scheme == "" || mediaURL.Host == "" {
|
|
return fmt.Errorf("invalid SENSE_MEDIAMTX_URL")
|
|
}
|
|
if mediaURL.User != nil {
|
|
return fmt.Errorf("SENSE_MEDIAMTX_URL must not contain credentials")
|
|
}
|
|
if c.ReconcileInterval <= 0 || c.ProbeInterval <= 0 {
|
|
return fmt.Errorf("loop intervals must be positive")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func stringEnv(name, fallback string) string {
|
|
if value, ok := os.LookupEnv(name); ok {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func boolEnv(name string, fallback bool) (bool, error) {
|
|
value, ok := os.LookupEnv(name)
|
|
if !ok {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return false, fmt.Errorf("invalid %s: %w", name, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func durationEnv(name string, fallback time.Duration) (time.Duration, error) {
|
|
value, ok := os.LookupEnv(name)
|
|
if !ok {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid %s: %w", name, err)
|
|
}
|
|
return parsed, nil
|
|
}
|