// Package config loads and validates the gateway's runtime configuration from // the process environment. Every variable is prefixed GATEWAY_. package config import ( "fmt" "os" "strconv" "time" ) // Config holds the gateway's runtime configuration. type Config struct { // HTTPAddr is the public Connect/h2c listener address (host:port). HTTPAddr string // AdminAddr is the admin reverse-proxy listener address. Admin is enabled only // when AdminUser and AdminPassword are also set. AdminAddr string // LogLevel is the zap log level: "debug", "info", "warn" or "error". LogLevel string // BackendHTTPURL is the base URL of the backend REST API (gateway -> backend). BackendHTTPURL string // BackendGRPCAddr is the backend push gRPC address the gateway subscribes to. BackendGRPCAddr string // BackendTimeout bounds a single backend REST call. BackendTimeout time.Duration // AdminUser and AdminPassword are the Basic-Auth credentials the gateway // checks before proxying admin traffic to the backend. Empty disables admin. AdminUser string AdminPassword string // ConnectorAddr is the gRPC address of the Telegram connector side-service. The // gateway calls it to validate Mini App initData and to deliver out-of-app push. // Empty disables the telegram auth path and the out-of-app push channel. ConnectorAddr string // SessionTTL bounds how long a resolved session stays cached; SessionCacheMax // caps the number of cached sessions. SessionTTL time.Duration SessionCacheMax int // PushHeartbeatInterval is the idle keep-alive cadence on a client live stream. PushHeartbeatInterval time.Duration // RateLimit configures the in-memory anti-abuse limiter. RateLimit RateLimitConfig } // RateLimitConfig holds the token-bucket limits per class. Public and admin are // keyed per client IP; the authenticated class is keyed per user id; the email // sub-limit guards the costly email-code path per IP. type RateLimitConfig struct { PublicPerMinute int PublicBurst int UserPerMinute int UserBurst int AdminPerMinute int AdminBurst int EmailPer10Min int EmailBurst int } // Defaults applied when the corresponding environment variable is unset. const ( defaultHTTPAddr = ":8081" defaultAdminAddr = ":8082" defaultLogLevel = "info" defaultBackendHTTPURL = "http://localhost:8080" defaultBackendGRPCAddr = "localhost:9090" defaultBackendTimeout = 5 * time.Second defaultSessionTTL = 10 * time.Minute defaultSessionCacheMax = 50000 defaultPushHeartbeatInterval = 15 * time.Second ) // DefaultRateLimit returns the built-in anti-abuse limits. func DefaultRateLimit() RateLimitConfig { return RateLimitConfig{ PublicPerMinute: 30, PublicBurst: 10, UserPerMinute: 120, UserBurst: 40, AdminPerMinute: 60, AdminBurst: 20, EmailPer10Min: 5, EmailBurst: 2, } } // Load reads the configuration from the environment, applies defaults, and // validates the result. func Load() (Config, error) { var err error c := Config{ HTTPAddr: envOr("GATEWAY_HTTP_ADDR", defaultHTTPAddr), AdminAddr: envOr("GATEWAY_ADMIN_ADDR", defaultAdminAddr), LogLevel: envOr("GATEWAY_LOG_LEVEL", defaultLogLevel), BackendHTTPURL: envOr("GATEWAY_BACKEND_HTTP_URL", defaultBackendHTTPURL), BackendGRPCAddr: envOr("GATEWAY_BACKEND_GRPC_ADDR", defaultBackendGRPCAddr), AdminUser: os.Getenv("GATEWAY_ADMIN_USER"), AdminPassword: os.Getenv("GATEWAY_ADMIN_PASSWORD"), ConnectorAddr: os.Getenv("GATEWAY_CONNECTOR_ADDR"), SessionCacheMax: defaultSessionCacheMax, RateLimit: DefaultRateLimit(), } if c.BackendTimeout, err = envDuration("GATEWAY_BACKEND_TIMEOUT", defaultBackendTimeout); err != nil { return Config{}, err } if c.SessionTTL, err = envDuration("GATEWAY_SESSION_TTL", defaultSessionTTL); err != nil { return Config{}, err } if c.SessionCacheMax, err = envInt("GATEWAY_SESSION_CACHE_MAX", defaultSessionCacheMax); err != nil { return Config{}, err } if c.PushHeartbeatInterval, err = envDuration("GATEWAY_PUSH_HEARTBEAT_INTERVAL", defaultPushHeartbeatInterval); err != nil { return Config{}, err } if err := c.validate(); err != nil { return Config{}, err } return c, nil } // AdminEnabled reports whether the admin proxy should be served (an address and // both Basic-Auth credentials are configured). func (c Config) AdminEnabled() bool { return c.AdminAddr != "" && c.AdminUser != "" && c.AdminPassword != "" } // validate reports whether the configuration values are acceptable. func (c Config) validate() error { switch c.LogLevel { case "debug", "info", "warn", "error": default: return fmt.Errorf("config: invalid GATEWAY_LOG_LEVEL %q", c.LogLevel) } if c.HTTPAddr == "" { return fmt.Errorf("config: GATEWAY_HTTP_ADDR must not be empty") } if c.BackendHTTPURL == "" { return fmt.Errorf("config: GATEWAY_BACKEND_HTTP_URL must not be empty") } if c.BackendGRPCAddr == "" { return fmt.Errorf("config: GATEWAY_BACKEND_GRPC_ADDR must not be empty") } return nil } // envOr returns the value of the environment variable named key, or fallback // when the variable is unset or empty. func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // envInt parses the environment variable named key as an int, returning fallback // when it is unset and an error when it is set but malformed. func envInt(key string, fallback int) (int, error) { v := os.Getenv(key) if v == "" { return fallback, nil } n, err := strconv.Atoi(v) if err != nil { return 0, fmt.Errorf("config: %s: %w", key, err) } return n, nil } // envDuration parses the environment variable named key as a Go duration, // returning fallback when it is unset and an error when it is set but malformed. func envDuration(key string, fallback time.Duration) (time.Duration, error) { v := os.Getenv(key) if v == "" { return fallback, nil } d, err := time.ParseDuration(v) if err != nil { return 0, fmt.Errorf("config: %s: %w", key, err) } return d, nil }