122 lines
3.7 KiB
Go
122 lines
3.7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
DefaultIPv4Primary = "https://api.ipify.org"
|
|
DefaultIPv4Fallback = "https://ifconfig.co/ip"
|
|
DefaultIPv6Primary = "https://api6.ipify.org"
|
|
DefaultIPv6Fallback = "https://ifconfig.co/ip"
|
|
)
|
|
|
|
type Config struct {
|
|
Address string
|
|
DataDir string
|
|
DatabasePath string
|
|
MasterKeyFile string
|
|
CookieSecure bool
|
|
SessionTTL time.Duration
|
|
HCloudBaseURL string
|
|
Mode string
|
|
ShutdownTimeout time.Duration
|
|
UpdateAPIURL string
|
|
UpdateAgentSock string
|
|
UpdateInterval time.Duration
|
|
}
|
|
|
|
func Load() (Config, error) {
|
|
dataDir := value("APP_DATA_DIR", "/data")
|
|
address := value("APP_ADDRESS", ":8080")
|
|
if _, _, err := net.SplitHostPort(address); err != nil {
|
|
return Config{}, fmt.Errorf("APP_ADDRESS must be host:port: %w", err)
|
|
}
|
|
|
|
mode := strings.ToLower(value("APP_MODE", "lan"))
|
|
if mode != "lan" && mode != "https" {
|
|
return Config{}, fmt.Errorf("APP_MODE must be either lan or https")
|
|
}
|
|
|
|
ttl, err := duration("APP_SESSION_TTL", 8*time.Hour)
|
|
if err != nil || ttl < time.Hour || ttl > 7*24*time.Hour {
|
|
return Config{}, fmt.Errorf("APP_SESSION_TTL must be between 1h and 168h")
|
|
}
|
|
shutdownTimeout, err := duration("APP_SHUTDOWN_TIMEOUT", 15*time.Second)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("APP_SHUTDOWN_TIMEOUT: %w", err)
|
|
}
|
|
|
|
keyFile := value("APP_MASTER_KEY_FILE", "/secrets/master_key")
|
|
updateInterval, err := duration("APP_SOFTWARE_UPDATE_CHECK_INTERVAL", 6*time.Hour)
|
|
if err != nil || updateInterval < 15*time.Minute || updateInterval > 7*24*time.Hour {
|
|
return Config{}, fmt.Errorf("APP_SOFTWARE_UPDATE_CHECK_INTERVAL must be between 15m and 168h")
|
|
}
|
|
updateAPIURL := value("APP_SOFTWARE_UPDATE_API_URL", "https://git.j-mahler.de/api/v1/repos/admin/hetzner-api-dyndns-records-webinterface/releases/latest")
|
|
if err := validateUpdateURL(updateAPIURL); err != nil {
|
|
return Config{}, fmt.Errorf("APP_SOFTWARE_UPDATE_API_URL: %w", err)
|
|
}
|
|
|
|
cookieSecure := mode == "https"
|
|
if raw := os.Getenv("APP_COOKIE_SECURE"); raw != "" {
|
|
parsed, parseErr := strconv.ParseBool(raw)
|
|
if parseErr != nil {
|
|
return Config{}, fmt.Errorf("APP_COOKIE_SECURE: %w", parseErr)
|
|
}
|
|
cookieSecure = parsed
|
|
}
|
|
|
|
return Config{
|
|
Address: address,
|
|
DataDir: dataDir,
|
|
DatabasePath: value("APP_DATABASE_PATH", filepath.Join(dataDir, "dyndns.db")),
|
|
MasterKeyFile: keyFile,
|
|
CookieSecure: cookieSecure,
|
|
SessionTTL: ttl,
|
|
HCloudBaseURL: strings.TrimRight(value("APP_HCLOUD_BASE_URL", "https://api.hetzner.cloud/v1"), "/"),
|
|
Mode: mode,
|
|
ShutdownTimeout: shutdownTimeout,
|
|
UpdateAPIURL: updateAPIURL,
|
|
UpdateAgentSock: value("APP_SOFTWARE_UPDATE_AGENT_SOCKET", "/run/domain-control-update/agent.sock"),
|
|
UpdateInterval: updateInterval,
|
|
}, nil
|
|
}
|
|
|
|
func validateUpdateURL(raw string) error {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil || parsed.Host == "" || parsed.User != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") {
|
|
return errors.New("must be an absolute HTTPS URL without embedded credentials")
|
|
}
|
|
if parsed.Scheme == "https" {
|
|
return nil
|
|
}
|
|
host := parsed.Hostname()
|
|
address := net.ParseIP(host)
|
|
if host == "localhost" || (address != nil && address.IsLoopback()) {
|
|
return nil
|
|
}
|
|
return errors.New("HTTP is allowed only for loopback test endpoints")
|
|
}
|
|
|
|
func value(name, fallback string) string {
|
|
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func duration(name string, fallback time.Duration) (time.Duration, error) {
|
|
if raw := strings.TrimSpace(os.Getenv(name)); raw != "" {
|
|
return time.ParseDuration(raw)
|
|
}
|
|
return fallback, nil
|
|
}
|