// Package config loads the game server configuration from environment // variables. package config import ( "fmt" "os" "strconv" "strings" "time" ) type Config struct { // ListenAddr is the address the websocket HTTP server binds to // (e.g. ":8081"). The server may sit behind a reverse proxy. ListenAddr string // PublicAddr is the public endpoint address of this server // (e.g. "ws://example.com:8081/ws"). It is used only to identify this // server: the "game_server" claim of a game token must match it // strictly. The main server must be configured to put exactly this // string into the tokens it issues. PublicAddr string // JWTSecret is the HS256 secret shared with the main server; both the // game token and the user token are verified with it. JWTSecret []byte // JoinTimeout is how long the first player waits for the second one // before the match is cancelled. JoinTimeout time.Duration // AllowedOrigins restricts the Origin header of websocket upgrade // requests; ["*"] disables the check. AllowedOrigins []string } func Load() (Config, error) { var cfg Config cfg.ListenAddr = getenv("LISTEN_ADDR", ":8081") cfg.PublicAddr = os.Getenv("PUBLIC_ADDR") if cfg.PublicAddr == "" { return cfg, fmt.Errorf("PUBLIC_ADDR is required (public endpoint address, e.g. ws://example.com:8081/ws)") } secret := os.Getenv("JWT_SECRET") if secret == "" { return cfg, fmt.Errorf("JWT_SECRET is required (HS256 secret shared with the main server)") } cfg.JWTSecret = []byte(secret) joinTimeout, err := parseDuration(getenv("JOIN_TIMEOUT", "2m")) if err != nil { return cfg, fmt.Errorf("invalid JOIN_TIMEOUT: %w", err) } cfg.JoinTimeout = joinTimeout origins := getenv("ALLOWED_ORIGINS", "*") cfg.AllowedOrigins = strings.Split(origins, ",") for i := range cfg.AllowedOrigins { cfg.AllowedOrigins[i] = strings.TrimSpace(cfg.AllowedOrigins[i]) } return cfg, nil } func getenv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // parseDuration accepts either a Go duration string ("90s", "2m") or a plain // number of seconds ("120"). func parseDuration(s string) (time.Duration, error) { if d, err := time.ParseDuration(s); err == nil { return d, nil } if secs, err := strconv.Atoi(s); err == nil { return time.Duration(secs) * time.Second, nil } return 0, fmt.Errorf("not a duration: %q", s) }