package config import ( "errors" "fmt" "net" "net/url" "os" "strconv" "strings" "time" ) type Config struct { ListenAddr string AppSecret string HTTPSOnly bool FrameAncestors []string TrustedProxies []*net.IPNet TSQueryHost string TSQueryPort int TSVoiceHost string TSVoicePort int TSQueryUser string TSQueryPassword string TSChannelAdminGID int TSQueryTimeout time.Duration RRDTool string RRDPath string GraphDir string StatsInterval time.Duration } func Load() (Config, error) { var c Config c.ListenAddr = env("LISTEN_ADDR", "127.0.0.1:8080") c.AppSecret = os.Getenv("APP_SECRET") if len(c.AppSecret) < 32 { return c, errors.New("APP_SECRET must contain at least 32 characters") } c.HTTPSOnly = envBool("HTTPS_ONLY", true) var err error c.FrameAncestors, err = frameAncestors(os.Getenv("FRAME_ANCESTORS")) if err != nil { return c, err } c.TrustedProxies, err = cidrs(os.Getenv("TRUSTED_PROXIES")) if err != nil { return c, err } c.TSQueryHost = env("TS_QUERY_HOST", "127.0.0.1") c.TSQueryPort, err = envInt("TS_QUERY_PORT", 10011) if err != nil { return c, err } c.TSVoiceHost = env("TS_VOICE_HOST", "linuxiarz.pl") c.TSVoicePort, err = envInt("TS_VOICE_PORT", 9987) if err != nil { return c, err } c.TSQueryUser = os.Getenv("TS_QUERY_USER") c.TSQueryPassword = os.Getenv("TS_QUERY_PASSWORD") if c.TSQueryUser == "" || c.TSQueryPassword == "" { return c, errors.New("TS_QUERY_USER and TS_QUERY_PASSWORD are required") } c.TSChannelAdminGID, err = envInt("TS_CHANNEL_ADMIN_GID", 5) if err != nil { return c, err } c.TSQueryTimeout, err = envDuration("TS_QUERY_TIMEOUT", 3*time.Second) if err != nil { return c, err } c.RRDTool = env("RRDTOOL", "rrdtool") c.RRDPath = env("RRD_PATH", "/var/lib/ts3monolith/linuxiarz.rrd") c.GraphDir = env("GRAPH_DIR", "/var/lib/ts3monolith/graphs") c.StatsInterval, err = envDuration("STATS_INTERVAL", time.Minute) if err != nil { return c, err } if c.StatsInterval < 10*time.Second { return c, errors.New("STATS_INTERVAL must be at least 10s") } return c, nil } func env(k, d string) string { if v := strings.TrimSpace(os.Getenv(k)); v != "" { return v } return d } func envBool(k string, d bool) bool { v := strings.TrimSpace(os.Getenv(k)) if v == "" { return d } b, err := strconv.ParseBool(v) if err != nil { return d } return b } func envInt(k string, d int) (int, error) { v := strings.TrimSpace(os.Getenv(k)) if v == "" { return d, nil } n, err := strconv.Atoi(v) if err != nil || n < 1 || n > 65535 { return 0, fmt.Errorf("invalid %s", k) } return n, nil } func envDuration(k string, d time.Duration) (time.Duration, error) { v := strings.TrimSpace(os.Getenv(k)) if v == "" { return d, nil } x, err := time.ParseDuration(v) if err != nil { return 0, fmt.Errorf("invalid %s: %w", k, err) } return x, nil } func frameAncestors(raw string) ([]string, error) { if strings.TrimSpace(raw) == "" { return []string{"'self'"}, nil } out := []string{"'self'"} for _, part := range strings.Fields(raw) { u, err := url.Parse(part) if err != nil || (u.Scheme != "https" && u.Scheme != "http") || u.Host == "" || u.Path != "" || u.RawQuery != "" || u.Fragment != "" { return nil, fmt.Errorf("invalid FRAME_ANCESTORS origin: %q", part) } out = append(out, u.Scheme+"://"+u.Host) } return out, nil } func cidrs(raw string) ([]*net.IPNet, error) { var out []*net.IPNet for _, part := range strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == ' ' || r == '\t' || r == '\n' }) { if strings.TrimSpace(part) == "" { continue } _, n, err := net.ParseCIDR(strings.TrimSpace(part)) if err != nil { return nil, fmt.Errorf("invalid TRUSTED_PROXIES entry %q", part) } out = append(out, n) } return out, nil }