Files
ts3-monolith-go/internal/config/config.go
T
2026-10-30 10:56:13 +01:00

162 lines
3.8 KiB
Go

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
}