// Package config loads process-level gateway configuration from environment // variables. package config import ( "fmt" "net/url" "os" "strconv" "strings" "time" "galaxy/redisconn" ) const gatewayRedisEnvPrefix = "GATEWAY" const ( // shutdownTimeoutEnvVar names the environment variable that controls the // maximum time granted to each component shutdown call. shutdownTimeoutEnvVar = "GATEWAY_SHUTDOWN_TIMEOUT" // logLevelEnvVar names the environment variable that configures the process // log level used by structured JSON logging. logLevelEnvVar = "GATEWAY_LOG_LEVEL" // publicHTTPAddrEnvVar names the environment variable that configures the // public REST listener address. publicHTTPAddrEnvVar = "GATEWAY_PUBLIC_HTTP_ADDR" // publicHTTPReadHeaderTimeoutEnvVar names the environment variable that // configures the maximum time allowed to read public REST request headers. publicHTTPReadHeaderTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_READ_HEADER_TIMEOUT" // publicHTTPReadTimeoutEnvVar names the environment variable that configures // the maximum time allowed to read the full public REST request. publicHTTPReadTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_READ_TIMEOUT" // publicHTTPIdleTimeoutEnvVar names the environment variable that configures // the keep-alive idle timeout for the public REST listener. publicHTTPIdleTimeoutEnvVar = "GATEWAY_PUBLIC_HTTP_IDLE_TIMEOUT" // publicAuthUpstreamTimeoutEnvVar names the environment variable that // configures the timeout budget used for public auth upstream calls. publicAuthUpstreamTimeoutEnvVar = "GATEWAY_PUBLIC_AUTH_UPSTREAM_TIMEOUT" // backendHTTPURLEnvVar names the environment variable that configures // the absolute base URL of the consolidated backend HTTP listener used // for public auth, internal session lookup, and authenticated user / // lobby commands. backendHTTPURLEnvVar = "GATEWAY_BACKEND_HTTP_URL" // backendGRPCPushURLEnvVar names the environment variable that // configures the dial target of backend's gRPC `Push.SubscribePush` // listener. backendGRPCPushURLEnvVar = "GATEWAY_BACKEND_GRPC_PUSH_URL" // backendGatewayClientIDEnvVar names the environment variable that // configures the durable identifier this gateway instance presents to // backend in `GatewaySubscribeRequest.gateway_client_id`. backendGatewayClientIDEnvVar = "GATEWAY_BACKEND_GATEWAY_CLIENT_ID" // backendHTTPTimeoutEnvVar names the environment variable that // configures the per-call timeout applied to backend HTTP requests. backendHTTPTimeoutEnvVar = "GATEWAY_BACKEND_HTTP_TIMEOUT" // backendPushReconnectBaseBackoffEnvVar names the environment variable // that configures the starting delay between reconnect attempts of the // gRPC SubscribePush stream. backendPushReconnectBaseBackoffEnvVar = "GATEWAY_BACKEND_PUSH_RECONNECT_BASE_BACKOFF" // backendPushReconnectMaxBackoffEnvVar names the environment variable // that configures the upper bound for exponential reconnect delays. backendPushReconnectMaxBackoffEnvVar = "GATEWAY_BACKEND_PUSH_RECONNECT_MAX_BACKOFF" // adminHTTPAddrEnvVar names the environment variable that configures the // private admin HTTP listener address. When it is empty, the admin listener // remains disabled. adminHTTPAddrEnvVar = "GATEWAY_ADMIN_HTTP_ADDR" // adminHTTPReadHeaderTimeoutEnvVar names the environment variable that // configures the maximum time allowed to read admin listener request // headers. adminHTTPReadHeaderTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_READ_HEADER_TIMEOUT" // adminHTTPReadTimeoutEnvVar names the environment variable that configures // the maximum time allowed to read one admin listener request. adminHTTPReadTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_READ_TIMEOUT" // adminHTTPIdleTimeoutEnvVar names the environment variable that configures // the keep-alive idle timeout for the admin listener. adminHTTPIdleTimeoutEnvVar = "GATEWAY_ADMIN_HTTP_IDLE_TIMEOUT" // authenticatedGRPCAddrEnvVar names the environment variable that configures // the authenticated gRPC listener address. authenticatedGRPCAddrEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ADDR" // authenticatedGRPCConnectionTimeoutEnvVar names the environment variable // that configures the inbound connection handshake timeout for the // authenticated gRPC listener. authenticatedGRPCConnectionTimeoutEnvVar = "GATEWAY_AUTHENTICATED_GRPC_CONNECTION_TIMEOUT" // authenticatedGRPCDownstreamTimeoutEnvVar names the environment variable // that configures the timeout budget used for downstream unary execution. authenticatedGRPCDownstreamTimeoutEnvVar = "GATEWAY_AUTHENTICATED_DOWNSTREAM_TIMEOUT" // authenticatedGRPCFreshnessWindowEnvVar names the environment variable that // configures the accepted client timestamp skew window for authenticated // gRPC requests. authenticatedGRPCFreshnessWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_FRESHNESS_WINDOW" // authenticatedGRPCIPRateLimitRequestsEnvVar names the environment // variable that configures the authenticated gRPC per-IP request budget per // window. authenticatedGRPCIPRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_REQUESTS" // authenticatedGRPCIPRateLimitWindowEnvVar names the environment variable // that configures the authenticated gRPC per-IP rate-limit window. authenticatedGRPCIPRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_WINDOW" // authenticatedGRPCIPRateLimitBurstEnvVar names the environment variable // that configures the authenticated gRPC per-IP rate-limit burst. authenticatedGRPCIPRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_IP_RATE_LIMIT_BURST" // authenticatedGRPCSessionRateLimitRequestsEnvVar names the environment // variable that configures the authenticated gRPC per-session request // budget per window. authenticatedGRPCSessionRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_REQUESTS" // authenticatedGRPCSessionRateLimitWindowEnvVar names the environment // variable that configures the authenticated gRPC per-session rate-limit // window. authenticatedGRPCSessionRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_WINDOW" // authenticatedGRPCSessionRateLimitBurstEnvVar names the environment // variable that configures the authenticated gRPC per-session rate-limit // burst. authenticatedGRPCSessionRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_SESSION_RATE_LIMIT_BURST" // authenticatedGRPCUserRateLimitRequestsEnvVar names the environment // variable that configures the authenticated gRPC per-user request budget // per window. authenticatedGRPCUserRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_REQUESTS" // authenticatedGRPCUserRateLimitWindowEnvVar names the environment // variable that configures the authenticated gRPC per-user rate-limit // window. authenticatedGRPCUserRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_WINDOW" // authenticatedGRPCUserRateLimitBurstEnvVar names the environment variable // that configures the authenticated gRPC per-user rate-limit burst. authenticatedGRPCUserRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_USER_RATE_LIMIT_BURST" // authenticatedGRPCMessageClassRateLimitRequestsEnvVar names the // environment variable that configures the authenticated gRPC per-message // class request budget per window. authenticatedGRPCMessageClassRateLimitRequestsEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_REQUESTS" // authenticatedGRPCMessageClassRateLimitWindowEnvVar names the environment // variable that configures the authenticated gRPC per-message-class // rate-limit window. authenticatedGRPCMessageClassRateLimitWindowEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_WINDOW" // authenticatedGRPCMessageClassRateLimitBurstEnvVar names the environment // variable that configures the authenticated gRPC per-message-class // rate-limit burst. authenticatedGRPCMessageClassRateLimitBurstEnvVar = "GATEWAY_AUTHENTICATED_GRPC_ANTI_ABUSE_MESSAGE_CLASS_RATE_LIMIT_BURST" // replayRedisKeyPrefixEnvVar names the environment variable that configures // the Redis key prefix used for authenticated replay reservations. replayRedisKeyPrefixEnvVar = "GATEWAY_REPLAY_REDIS_KEY_PREFIX" // replayRedisReserveTimeoutEnvVar names the environment variable that // configures the timeout used for authenticated replay reservations and // startup connectivity checks. replayRedisReserveTimeoutEnvVar = "GATEWAY_REPLAY_REDIS_RESERVE_TIMEOUT" // responseSignerPrivateKeyPEMPathEnvVar names the environment variable that // configures the path to the PKCS#8 PEM-encoded Ed25519 private key used to // sign authenticated unary responses and stream events. responseSignerPrivateKeyPEMPathEnvVar = "GATEWAY_RESPONSE_SIGNER_PRIVATE_KEY_PEM_PATH" // publicAuthMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for public_auth. publicAuthMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_MAX_BODY_BYTES" // publicAuthRateLimitRequestsEnvVar names the environment variable that // configures the public_auth request budget per window. publicAuthRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_REQUESTS" // publicAuthRateLimitWindowEnvVar names the environment variable that // configures the public_auth rate-limit window. publicAuthRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_WINDOW" // publicAuthRateLimitBurstEnvVar names the environment variable that // configures the public_auth rate-limit burst. publicAuthRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_AUTH_RATE_LIMIT_BURST" // browserBootstrapMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for browser_bootstrap. browserBootstrapMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_MAX_BODY_BYTES" // browserBootstrapRateLimitRequestsEnvVar names the environment variable // that configures the browser_bootstrap request budget per window. browserBootstrapRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_REQUESTS" // browserBootstrapRateLimitWindowEnvVar names the environment variable that // configures the browser_bootstrap rate-limit window. browserBootstrapRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_WINDOW" // browserBootstrapRateLimitBurstEnvVar names the environment variable that // configures the browser_bootstrap rate-limit burst. browserBootstrapRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_BOOTSTRAP_RATE_LIMIT_BURST" // browserAssetMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for browser_asset. browserAssetMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_MAX_BODY_BYTES" // browserAssetRateLimitRequestsEnvVar names the environment variable that // configures the browser_asset request budget per window. browserAssetRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_REQUESTS" // browserAssetRateLimitWindowEnvVar names the environment variable that // configures the browser_asset rate-limit window. browserAssetRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_WINDOW" // browserAssetRateLimitBurstEnvVar names the environment variable that // configures the browser_asset rate-limit burst. browserAssetRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_BROWSER_ASSET_RATE_LIMIT_BURST" // publicMiscMaxBodyBytesEnvVar names the environment variable that // configures the maximum accepted request body size for public_misc. publicMiscMaxBodyBytesEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_MAX_BODY_BYTES" // publicMiscRateLimitRequestsEnvVar names the environment variable that // configures the public_misc request budget per window. publicMiscRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_REQUESTS" // publicMiscRateLimitWindowEnvVar names the environment variable that // configures the public_misc rate-limit window. publicMiscRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_WINDOW" // publicMiscRateLimitBurstEnvVar names the environment variable that // configures the public_misc rate-limit burst. publicMiscRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_PUBLIC_MISC_RATE_LIMIT_BURST" // sendEmailCodeIdentityRateLimitRequestsEnvVar names the environment // variable that configures the send-email-code identity request budget per // window. sendEmailCodeIdentityRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS" // sendEmailCodeIdentityRateLimitWindowEnvVar names the environment variable // that configures the send-email-code identity rate-limit window. sendEmailCodeIdentityRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_WINDOW" // sendEmailCodeIdentityRateLimitBurstEnvVar names the environment variable // that configures the send-email-code identity rate-limit burst. sendEmailCodeIdentityRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_SEND_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST" // confirmEmailCodeIdentityRateLimitRequestsEnvVar names the environment // variable that configures the confirm-email-code identity request budget // per window. confirmEmailCodeIdentityRateLimitRequestsEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_REQUESTS" // confirmEmailCodeIdentityRateLimitWindowEnvVar names the environment // variable that configures the confirm-email-code identity rate-limit // window. confirmEmailCodeIdentityRateLimitWindowEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_WINDOW" // confirmEmailCodeIdentityRateLimitBurstEnvVar names the environment // variable that configures the confirm-email-code identity rate-limit burst. confirmEmailCodeIdentityRateLimitBurstEnvVar = "GATEWAY_PUBLIC_HTTP_ANTI_ABUSE_CONFIRM_EMAIL_CODE_IDENTITY_RATE_LIMIT_BURST" // defaultShutdownTimeout is applied when shutdownTimeoutEnvVar is absent. defaultShutdownTimeout = 5 * time.Second // defaultLogLevel is applied when logLevelEnvVar is absent. defaultLogLevel = "info" // defaultPublicHTTPAddr is applied when publicHTTPAddrEnvVar is absent. defaultPublicHTTPAddr = ":8080" defaultPublicHTTPReadHeaderTimeout = 2 * time.Second defaultPublicHTTPReadTimeout = 10 * time.Second defaultPublicHTTPIdleTimeout = time.Minute defaultPublicAuthUpstreamTimeout = 3 * time.Second defaultAdminHTTPReadHeaderTimeout = 2 * time.Second defaultAdminHTTPReadTimeout = 10 * time.Second defaultAdminHTTPIdleTimeout = time.Minute // defaultAuthenticatedGRPCAddr is applied when // authenticatedGRPCAddrEnvVar is absent. defaultAuthenticatedGRPCAddr = ":9090" defaultAuthenticatedGRPCConnectionTimeout = 5 * time.Second defaultAuthenticatedGRPCDownstreamTimeout = 5 * time.Second defaultAuthenticatedGRPCFreshnessWindow = 5 * time.Minute defaultAuthenticatedGRPCIPRateLimitRequests = 120 defaultAuthenticatedGRPCIPRateLimitBurst = 40 defaultAuthenticatedGRPCSessionRateLimitRequests = 60 defaultAuthenticatedGRPCSessionRateLimitBurst = 20 defaultAuthenticatedGRPCUserRateLimitRequests = 120 defaultAuthenticatedGRPCUserRateLimitBurst = 40 defaultAuthenticatedGRPCMessageClassRateLimitRequests = 60 defaultAuthenticatedGRPCMessageClassRateLimitBurst = 20 defaultReplayRedisKeyPrefix = "gateway:replay:" defaultReplayRedisReserveTimeout = 250 * time.Millisecond defaultBackendHTTPTimeout = 5 * time.Second defaultBackendPushReconnectBaseBackoff = 250 * time.Millisecond defaultBackendPushReconnectMaxBackoff = 30 * time.Second defaultPublicAuthMaxBodyBytes = int64(8192) defaultPublicAuthRateLimitRequests = 30 defaultPublicAuthRateLimitBurst = 10 defaultBrowserBootstrapRateLimitRequests = 60 defaultBrowserBootstrapRateLimitBurst = 20 defaultBrowserAssetRateLimitRequests = 300 defaultBrowserAssetRateLimitBurst = 80 defaultPublicMiscRateLimitRequests = 30 defaultPublicMiscRateLimitBurst = 10 defaultSendEmailCodeIdentityRateLimitRequests = 3 defaultSendEmailCodeIdentityRateLimitBurst = 1 defaultConfirmEmailCodeIdentityRateLimitRequests = 6 defaultConfirmEmailCodeIdentityRateLimitBurst = 2 ) var ( defaultClassRateLimitWindow = time.Minute defaultIdentityRateLimitWindow = 10 * time.Minute ) // RateLimitConfig describes a single rate-limit budget. type RateLimitConfig struct { // Requests is the number of accepted requests replenished per Window. Requests int // Window is the interval over which Requests are replenished. Window time.Duration // Burst is the maximum number of immediately available tokens. Burst int } // PublicRateLimitConfig identifies the generic rate-limit budget shape used by // public REST policy. type PublicRateLimitConfig = RateLimitConfig // AuthenticatedRateLimitConfig identifies the generic rate-limit budget shape // used by authenticated gRPC policy. type AuthenticatedRateLimitConfig = RateLimitConfig // PublicRoutePolicyConfig describes the anti-abuse policy enforced for one // stable public REST traffic class. type PublicRoutePolicyConfig struct { // MaxBodyBytes is the maximum accepted request body size. Zero means that // the request must not carry a body. MaxBodyBytes int64 // RateLimit configures the per-IP budget for the route class. RateLimit PublicRateLimitConfig } // PublicAuthIdentityPolicyConfig describes the additional identity-based // limiter applied to one public auth command. type PublicAuthIdentityPolicyConfig struct { // RateLimit configures the accepted request budget for one normalized public // auth identity key. RateLimit PublicRateLimitConfig } // PublicHTTPAntiAbuseConfig describes the public REST anti-abuse policy used // before route handling. type PublicHTTPAntiAbuseConfig struct { // PublicAuth applies to the stable public_auth route class. PublicAuth PublicRoutePolicyConfig // BrowserBootstrap applies to the stable browser_bootstrap route class. BrowserBootstrap PublicRoutePolicyConfig // BrowserAsset applies to the stable browser_asset route class. BrowserAsset PublicRoutePolicyConfig // PublicMisc applies to the stable public_misc route class. PublicMisc PublicRoutePolicyConfig // SendEmailCodeIdentity applies the additional identity limiter for // send-email-code. SendEmailCodeIdentity PublicAuthIdentityPolicyConfig // ConfirmEmailCodeIdentity applies the additional identity limiter for // confirm-email-code. ConfirmEmailCodeIdentity PublicAuthIdentityPolicyConfig } // AuthenticatedGRPCAntiAbuseConfig describes the authenticated gRPC // rate-limit budgets enforced after request authenticity has been established. type AuthenticatedGRPCAntiAbuseConfig struct { // IP applies to the transport peer IP derived from the gRPC connection. IP AuthenticatedRateLimitConfig // Session applies to the authenticated device_session_id. Session AuthenticatedRateLimitConfig // User applies to the authenticated user_id resolved from SessionCache. User AuthenticatedRateLimitConfig // MessageClass applies to the current authenticated message class. The // gateway uses the full message_type literal as the stable v1 class key. MessageClass AuthenticatedRateLimitConfig } // PublicHTTPConfig describes the public unauthenticated REST listener exposed // by the gateway. type PublicHTTPConfig struct { // Addr is the TCP listen address used by the public REST server. Addr string // ReadHeaderTimeout bounds how long the listener may spend reading request // headers before the gateway rejects the connection. ReadHeaderTimeout time.Duration // ReadTimeout bounds how long the listener may spend reading one public // request. ReadTimeout time.Duration // IdleTimeout bounds how long the listener keeps an idle keep-alive // connection open. IdleTimeout time.Duration // AuthUpstreamTimeout bounds one public auth adapter call. AuthUpstreamTimeout time.Duration // AntiAbuse configures the public REST anti-abuse middleware. AntiAbuse PublicHTTPAntiAbuseConfig } // BackendConfig describes the consolidated backend service the gateway // talks to. Every authenticated and public HTTP request is forwarded to // `HTTPBaseURL`; the gRPC `Push.SubscribePush` stream is opened against // `GRPCPushURL`. type BackendConfig struct { // HTTPBaseURL is the absolute base URL of the backend HTTP listener // (`/api/v1/{public,user,internal}/*`). Required. HTTPBaseURL string // GRPCPushURL is the dial target of the backend `Push.SubscribePush` // listener (`host:port`). Required. GRPCPushURL string // GatewayClientID is the durable identifier this gateway instance // presents to backend in `GatewaySubscribeRequest.gateway_client_id`. // Required. GatewayClientID string // HTTPTimeout bounds individual REST calls. Must be positive. HTTPTimeout time.Duration // PushReconnectBaseBackoff is the starting delay between reconnect // attempts of `Push.SubscribePush`. Must be positive. PushReconnectBaseBackoff time.Duration // PushReconnectMaxBackoff is the upper bound for exponential // reconnect delays. Must be greater than or equal to // PushReconnectBaseBackoff. PushReconnectMaxBackoff time.Duration } // AdminHTTPConfig describes the private operational HTTP listener used for // metrics exposure. The listener remains disabled when Addr is empty. type AdminHTTPConfig struct { // Addr is the TCP listen address used by the admin HTTP server. An empty // value disables the listener. Addr string // ReadHeaderTimeout bounds how long the listener may spend reading request // headers before the gateway rejects the connection. ReadHeaderTimeout time.Duration // ReadTimeout bounds how long the listener may spend reading one admin // request. ReadTimeout time.Duration // IdleTimeout bounds how long the listener keeps an idle keep-alive // connection open. IdleTimeout time.Duration } // AuthenticatedGRPCConfig describes the authenticated gRPC listener exposed by // the gateway. type AuthenticatedGRPCConfig struct { // Addr is the TCP listen address used by the authenticated gRPC server. Addr string // ConnectionTimeout bounds one inbound connection handshake. ConnectionTimeout time.Duration // DownstreamTimeout bounds one downstream unary execution after the request // has passed the full authenticated ingress pipeline. DownstreamTimeout time.Duration // FreshnessWindow is the accepted skew window around current server time // used for client request timestamps. FreshnessWindow time.Duration // AntiAbuse configures the authenticated gRPC rate limits enforced after // the request passes the transport authenticity checks. AntiAbuse AuthenticatedGRPCAntiAbuseConfig } // ReplayRedisConfig describes the Redis namespace and timeout used for // authenticated replay reservations. type ReplayRedisConfig struct { // KeyPrefix is prepended to every ReplayStore Redis key. KeyPrefix string // ReserveTimeout bounds individual ReplayStore Redis operations. ReserveTimeout time.Duration } // ResponseSignerConfig describes the private-key material used to sign // authenticated unary responses and stream events. type ResponseSignerConfig struct { // PrivateKeyPEMPath is the filesystem path to the PKCS#8 PEM-encoded // Ed25519 private key loaded during startup. PrivateKeyPEMPath string } // LoggingConfig describes the process-wide structured logging settings. type LoggingConfig struct { // Level is the configured minimum log level literal. Level string } // Config describes process-wide settings required to start and stop the // gateway safely. type Config struct { // ShutdownTimeout limits how long each component may spend in Shutdown // before the gateway reports a timeout. ShutdownTimeout time.Duration // Logging configures the process-wide structured logger. Logging LoggingConfig // PublicHTTP configures the public unauthenticated REST listener. PublicHTTP PublicHTTPConfig // Backend configures the consolidated backend the gateway forwards // every public auth and authenticated user/lobby request to and the // gRPC `Push.SubscribePush` stream consumed for inbound events. Backend BackendConfig // AdminHTTP configures the optional private admin listener used for metrics // exposure. AdminHTTP AdminHTTPConfig // AuthenticatedGRPC configures the authenticated gRPC listener. AuthenticatedGRPC AuthenticatedGRPCConfig // Redis carries the master/replica/password connection topology used // by the anti-replay reservation store, sourced from the // GATEWAY_REDIS_* environment variables managed by `pkg/redisconn`. // The implementation dropped session cache projection and the two Redis // Streams; Redis is now used only for replay reservations. Redis redisconn.Config // ReplayRedis configures the Redis-backed authenticated ReplayStore. ReplayRedis ReplayRedisConfig // ResponseSigner configures the authenticated response and event signer // loaded during startup. ResponseSigner ResponseSignerConfig } // DefaultPublicHTTPConfig returns the default listener and anti-abuse settings // for the public REST surface. func DefaultPublicHTTPConfig() PublicHTTPConfig { return PublicHTTPConfig{ Addr: defaultPublicHTTPAddr, ReadHeaderTimeout: defaultPublicHTTPReadHeaderTimeout, ReadTimeout: defaultPublicHTTPReadTimeout, IdleTimeout: defaultPublicHTTPIdleTimeout, AuthUpstreamTimeout: defaultPublicAuthUpstreamTimeout, AntiAbuse: PublicHTTPAntiAbuseConfig{ PublicAuth: PublicRoutePolicyConfig{ MaxBodyBytes: defaultPublicAuthMaxBodyBytes, RateLimit: PublicRateLimitConfig{ Requests: defaultPublicAuthRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultPublicAuthRateLimitBurst, }, }, BrowserBootstrap: PublicRoutePolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultBrowserBootstrapRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultBrowserBootstrapRateLimitBurst, }, }, BrowserAsset: PublicRoutePolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultBrowserAssetRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultBrowserAssetRateLimitBurst, }, }, PublicMisc: PublicRoutePolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultPublicMiscRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultPublicMiscRateLimitBurst, }, }, SendEmailCodeIdentity: PublicAuthIdentityPolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultSendEmailCodeIdentityRateLimitRequests, Window: defaultIdentityRateLimitWindow, Burst: defaultSendEmailCodeIdentityRateLimitBurst, }, }, ConfirmEmailCodeIdentity: PublicAuthIdentityPolicyConfig{ RateLimit: PublicRateLimitConfig{ Requests: defaultConfirmEmailCodeIdentityRateLimitRequests, Window: defaultIdentityRateLimitWindow, Burst: defaultConfirmEmailCodeIdentityRateLimitBurst, }, }, }, } } // DefaultAdminHTTPConfig returns the default settings for the optional private // admin listener. The zero address keeps the listener disabled by default. func DefaultAdminHTTPConfig() AdminHTTPConfig { return AdminHTTPConfig{ ReadHeaderTimeout: defaultAdminHTTPReadHeaderTimeout, ReadTimeout: defaultAdminHTTPReadTimeout, IdleTimeout: defaultAdminHTTPIdleTimeout, } } // DefaultAuthenticatedGRPCConfig returns the default listener, freshness, and // anti-abuse settings for the authenticated gRPC surface. func DefaultAuthenticatedGRPCConfig() AuthenticatedGRPCConfig { return AuthenticatedGRPCConfig{ Addr: defaultAuthenticatedGRPCAddr, ConnectionTimeout: defaultAuthenticatedGRPCConnectionTimeout, DownstreamTimeout: defaultAuthenticatedGRPCDownstreamTimeout, FreshnessWindow: defaultAuthenticatedGRPCFreshnessWindow, AntiAbuse: AuthenticatedGRPCAntiAbuseConfig{ IP: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCIPRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCIPRateLimitBurst, }, Session: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCSessionRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCSessionRateLimitBurst, }, User: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCUserRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCUserRateLimitBurst, }, MessageClass: AuthenticatedRateLimitConfig{ Requests: defaultAuthenticatedGRPCMessageClassRateLimitRequests, Window: defaultClassRateLimitWindow, Burst: defaultAuthenticatedGRPCMessageClassRateLimitBurst, }, }, } } // DefaultLoggingConfig returns the default structured logging settings. func DefaultLoggingConfig() LoggingConfig { return LoggingConfig{Level: defaultLogLevel} } // DefaultReplayRedisConfig returns the default Redis key namespace and timeout // used for authenticated replay reservations. func DefaultReplayRedisConfig() ReplayRedisConfig { return ReplayRedisConfig{ KeyPrefix: defaultReplayRedisKeyPrefix, ReserveTimeout: defaultReplayRedisReserveTimeout, } } // DefaultBackendConfig returns the default backend settings used for the // gateway → backend HTTP and gRPC conversation. URL fields stay empty and // must be supplied explicitly via env vars. func DefaultBackendConfig() BackendConfig { return BackendConfig{ HTTPTimeout: defaultBackendHTTPTimeout, PushReconnectBaseBackoff: defaultBackendPushReconnectBaseBackoff, PushReconnectMaxBackoff: defaultBackendPushReconnectMaxBackoff, } } // DefaultResponseSignerConfig returns the default response-signer settings. // The private key path remains empty and must be supplied explicitly. func DefaultResponseSignerConfig() ResponseSignerConfig { return ResponseSignerConfig{} } // LoadFromEnv loads Config from the process environment, applies defaults for // omitted settings, and validates the resulting values. func LoadFromEnv() (Config, error) { cfg := Config{ ShutdownTimeout: defaultShutdownTimeout, Logging: DefaultLoggingConfig(), PublicHTTP: DefaultPublicHTTPConfig(), Backend: DefaultBackendConfig(), AdminHTTP: DefaultAdminHTTPConfig(), AuthenticatedGRPC: DefaultAuthenticatedGRPCConfig(), Redis: redisconn.DefaultConfig(), ReplayRedis: DefaultReplayRedisConfig(), ResponseSigner: DefaultResponseSignerConfig(), } rawShutdownTimeout, ok := os.LookupEnv(shutdownTimeoutEnvVar) if ok { shutdownTimeout, err := time.ParseDuration(rawShutdownTimeout) if err != nil { return Config{}, fmt.Errorf("load gateway config: parse %s: %w", shutdownTimeoutEnvVar, err) } cfg.ShutdownTimeout = shutdownTimeout } rawLogLevel, ok := os.LookupEnv(logLevelEnvVar) if ok { cfg.Logging.Level = rawLogLevel } rawPublicHTTPAddr, ok := os.LookupEnv(publicHTTPAddrEnvVar) if ok { cfg.PublicHTTP.Addr = rawPublicHTTPAddr } publicHTTPReadHeaderTimeout, err := loadDurationEnvWithDefault(publicHTTPReadHeaderTimeoutEnvVar, cfg.PublicHTTP.ReadHeaderTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.ReadHeaderTimeout = publicHTTPReadHeaderTimeout publicHTTPReadTimeout, err := loadDurationEnvWithDefault(publicHTTPReadTimeoutEnvVar, cfg.PublicHTTP.ReadTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.ReadTimeout = publicHTTPReadTimeout publicHTTPIdleTimeout, err := loadDurationEnvWithDefault(publicHTTPIdleTimeoutEnvVar, cfg.PublicHTTP.IdleTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.IdleTimeout = publicHTTPIdleTimeout publicAuthUpstreamTimeout, err := loadDurationEnvWithDefault(publicAuthUpstreamTimeoutEnvVar, cfg.PublicHTTP.AuthUpstreamTimeout) if err != nil { return Config{}, err } cfg.PublicHTTP.AuthUpstreamTimeout = publicAuthUpstreamTimeout if v, ok := os.LookupEnv(backendHTTPURLEnvVar); ok { cfg.Backend.HTTPBaseURL = v } if v, ok := os.LookupEnv(backendGRPCPushURLEnvVar); ok { cfg.Backend.GRPCPushURL = v } if v, ok := os.LookupEnv(backendGatewayClientIDEnvVar); ok { cfg.Backend.GatewayClientID = v } backendHTTPTimeout, err := loadDurationEnvWithDefault(backendHTTPTimeoutEnvVar, cfg.Backend.HTTPTimeout) if err != nil { return Config{}, err } cfg.Backend.HTTPTimeout = backendHTTPTimeout backendPushReconnectBaseBackoff, err := loadDurationEnvWithDefault(backendPushReconnectBaseBackoffEnvVar, cfg.Backend.PushReconnectBaseBackoff) if err != nil { return Config{}, err } cfg.Backend.PushReconnectBaseBackoff = backendPushReconnectBaseBackoff backendPushReconnectMaxBackoff, err := loadDurationEnvWithDefault(backendPushReconnectMaxBackoffEnvVar, cfg.Backend.PushReconnectMaxBackoff) if err != nil { return Config{}, err } cfg.Backend.PushReconnectMaxBackoff = backendPushReconnectMaxBackoff rawAdminHTTPAddr, ok := os.LookupEnv(adminHTTPAddrEnvVar) if ok { cfg.AdminHTTP.Addr = rawAdminHTTPAddr } adminHTTPReadHeaderTimeout, err := loadDurationEnvWithDefault(adminHTTPReadHeaderTimeoutEnvVar, cfg.AdminHTTP.ReadHeaderTimeout) if err != nil { return Config{}, err } cfg.AdminHTTP.ReadHeaderTimeout = adminHTTPReadHeaderTimeout adminHTTPReadTimeout, err := loadDurationEnvWithDefault(adminHTTPReadTimeoutEnvVar, cfg.AdminHTTP.ReadTimeout) if err != nil { return Config{}, err } cfg.AdminHTTP.ReadTimeout = adminHTTPReadTimeout adminHTTPIdleTimeout, err := loadDurationEnvWithDefault(adminHTTPIdleTimeoutEnvVar, cfg.AdminHTTP.IdleTimeout) if err != nil { return Config{}, err } cfg.AdminHTTP.IdleTimeout = adminHTTPIdleTimeout rawAuthenticatedGRPCAddr, ok := os.LookupEnv(authenticatedGRPCAddrEnvVar) if ok { cfg.AuthenticatedGRPC.Addr = rawAuthenticatedGRPCAddr } authenticatedGRPCConnectionTimeout, err := loadDurationEnvWithDefault(authenticatedGRPCConnectionTimeoutEnvVar, cfg.AuthenticatedGRPC.ConnectionTimeout) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.ConnectionTimeout = authenticatedGRPCConnectionTimeout authenticatedGRPCDownstreamTimeout, err := loadDurationEnvWithDefault(authenticatedGRPCDownstreamTimeoutEnvVar, cfg.AuthenticatedGRPC.DownstreamTimeout) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.DownstreamTimeout = authenticatedGRPCDownstreamTimeout authenticatedGRPCFreshnessWindow, err := loadDurationEnvWithDefault(authenticatedGRPCFreshnessWindowEnvVar, cfg.AuthenticatedGRPC.FreshnessWindow) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.FreshnessWindow = authenticatedGRPCFreshnessWindow authenticatedGRPCIPRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.IP, authenticatedGRPCIPRateLimitRequestsEnvVar, authenticatedGRPCIPRateLimitWindowEnvVar, authenticatedGRPCIPRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.IP = authenticatedGRPCIPRateLimit authenticatedGRPCSessionRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.Session, authenticatedGRPCSessionRateLimitRequestsEnvVar, authenticatedGRPCSessionRateLimitWindowEnvVar, authenticatedGRPCSessionRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.Session = authenticatedGRPCSessionRateLimit authenticatedGRPCUserRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.User, authenticatedGRPCUserRateLimitRequestsEnvVar, authenticatedGRPCUserRateLimitWindowEnvVar, authenticatedGRPCUserRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.User = authenticatedGRPCUserRateLimit messageClassRateLimit, err := loadRateLimitConfigFromEnv( cfg.AuthenticatedGRPC.AntiAbuse.MessageClass, authenticatedGRPCMessageClassRateLimitRequestsEnvVar, authenticatedGRPCMessageClassRateLimitWindowEnvVar, authenticatedGRPCMessageClassRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.AuthenticatedGRPC.AntiAbuse.MessageClass = messageClassRateLimit redisConn, err := redisconn.LoadFromEnv(gatewayRedisEnvPrefix) if err != nil { return Config{}, err } cfg.Redis = redisConn rawReplayRedisKeyPrefix, ok := os.LookupEnv(replayRedisKeyPrefixEnvVar) if ok { cfg.ReplayRedis.KeyPrefix = rawReplayRedisKeyPrefix } replayRedisReserveTimeout, err := loadDurationEnvWithDefault(replayRedisReserveTimeoutEnvVar, cfg.ReplayRedis.ReserveTimeout) if err != nil { return Config{}, err } cfg.ReplayRedis.ReserveTimeout = replayRedisReserveTimeout rawSignerKeyPath, ok := os.LookupEnv(responseSignerPrivateKeyPEMPathEnvVar) if ok { cfg.ResponseSigner.PrivateKeyPEMPath = rawSignerKeyPath } publicAuthPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.PublicAuth, publicAuthMaxBodyBytesEnvVar, publicAuthRateLimitRequestsEnvVar, publicAuthRateLimitWindowEnvVar, publicAuthRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.PublicAuth = publicAuthPolicy browserBootstrapPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.BrowserBootstrap, browserBootstrapMaxBodyBytesEnvVar, browserBootstrapRateLimitRequestsEnvVar, browserBootstrapRateLimitWindowEnvVar, browserBootstrapRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.BrowserBootstrap = browserBootstrapPolicy browserAssetPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.BrowserAsset, browserAssetMaxBodyBytesEnvVar, browserAssetRateLimitRequestsEnvVar, browserAssetRateLimitWindowEnvVar, browserAssetRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.BrowserAsset = browserAssetPolicy publicMiscPolicy, err := loadPublicRoutePolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.PublicMisc, publicMiscMaxBodyBytesEnvVar, publicMiscRateLimitRequestsEnvVar, publicMiscRateLimitWindowEnvVar, publicMiscRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.PublicMisc = publicMiscPolicy sendIdentityPolicy, err := loadPublicAuthIdentityPolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity, sendEmailCodeIdentityRateLimitRequestsEnvVar, sendEmailCodeIdentityRateLimitWindowEnvVar, sendEmailCodeIdentityRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity = sendIdentityPolicy confirmIdentityPolicy, err := loadPublicAuthIdentityPolicyConfigFromEnv( cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity, confirmEmailCodeIdentityRateLimitRequestsEnvVar, confirmEmailCodeIdentityRateLimitWindowEnvVar, confirmEmailCodeIdentityRateLimitBurstEnvVar, ) if err != nil { return Config{}, err } cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity = confirmIdentityPolicy if cfg.ShutdownTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", shutdownTimeoutEnvVar) } if err := validateLogLevel(cfg.Logging.Level); err != nil { return Config{}, fmt.Errorf("load gateway config: %w", err) } if strings.TrimSpace(cfg.PublicHTTP.Addr) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", publicHTTPAddrEnvVar) } if cfg.PublicHTTP.ReadHeaderTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPReadHeaderTimeoutEnvVar) } if cfg.PublicHTTP.ReadTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPReadTimeoutEnvVar) } if cfg.PublicHTTP.IdleTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicHTTPIdleTimeoutEnvVar) } if cfg.PublicHTTP.AuthUpstreamTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", publicAuthUpstreamTimeoutEnvVar) } cfg.Backend.HTTPBaseURL = strings.TrimSpace(cfg.Backend.HTTPBaseURL) if cfg.Backend.HTTPBaseURL == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", backendHTTPURLEnvVar) } parsedBackendHTTP, err := url.Parse(strings.TrimRight(cfg.Backend.HTTPBaseURL, "/")) if err != nil { return Config{}, fmt.Errorf("load gateway config: parse %s: %w", backendHTTPURLEnvVar, err) } if parsedBackendHTTP.Scheme == "" || parsedBackendHTTP.Host == "" { return Config{}, fmt.Errorf("load gateway config: %s must be an absolute URL", backendHTTPURLEnvVar) } cfg.Backend.HTTPBaseURL = parsedBackendHTTP.String() cfg.Backend.GRPCPushURL = strings.TrimSpace(cfg.Backend.GRPCPushURL) if cfg.Backend.GRPCPushURL == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", backendGRPCPushURLEnvVar) } cfg.Backend.GatewayClientID = strings.TrimSpace(cfg.Backend.GatewayClientID) if cfg.Backend.GatewayClientID == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", backendGatewayClientIDEnvVar) } if cfg.Backend.HTTPTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", backendHTTPTimeoutEnvVar) } if cfg.Backend.PushReconnectBaseBackoff <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", backendPushReconnectBaseBackoffEnvVar) } if cfg.Backend.PushReconnectMaxBackoff < cfg.Backend.PushReconnectBaseBackoff { return Config{}, fmt.Errorf("load gateway config: %s must be >= %s", backendPushReconnectMaxBackoffEnvVar, backendPushReconnectBaseBackoffEnvVar) } if addr := strings.TrimSpace(cfg.AdminHTTP.Addr); addr != "" { cfg.AdminHTTP.Addr = addr } if cfg.AdminHTTP.ReadHeaderTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPReadHeaderTimeoutEnvVar) } if cfg.AdminHTTP.ReadTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPReadTimeoutEnvVar) } if cfg.AdminHTTP.IdleTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", adminHTTPIdleTimeoutEnvVar) } if strings.TrimSpace(cfg.AuthenticatedGRPC.Addr) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", authenticatedGRPCAddrEnvVar) } if cfg.AuthenticatedGRPC.ConnectionTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCConnectionTimeoutEnvVar) } if cfg.AuthenticatedGRPC.DownstreamTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCDownstreamTimeoutEnvVar) } if cfg.AuthenticatedGRPC.FreshnessWindow <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", authenticatedGRPCFreshnessWindowEnvVar) } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.IP, authenticatedGRPCIPRateLimitRequestsEnvVar, authenticatedGRPCIPRateLimitWindowEnvVar, authenticatedGRPCIPRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.Session, authenticatedGRPCSessionRateLimitRequestsEnvVar, authenticatedGRPCSessionRateLimitWindowEnvVar, authenticatedGRPCSessionRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.User, authenticatedGRPCUserRateLimitRequestsEnvVar, authenticatedGRPCUserRateLimitWindowEnvVar, authenticatedGRPCUserRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := validateRateLimitConfig( cfg.AuthenticatedGRPC.AntiAbuse.MessageClass, authenticatedGRPCMessageClassRateLimitRequestsEnvVar, authenticatedGRPCMessageClassRateLimitWindowEnvVar, authenticatedGRPCMessageClassRateLimitBurstEnvVar, ); err != nil { return Config{}, err } if err := cfg.Redis.Validate(); err != nil { return Config{}, fmt.Errorf("load gateway config: redis: %w", err) } if strings.TrimSpace(cfg.ReplayRedis.KeyPrefix) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", replayRedisKeyPrefixEnvVar) } if cfg.ReplayRedis.ReserveTimeout <= 0 { return Config{}, fmt.Errorf("load gateway config: %s must be positive", replayRedisReserveTimeoutEnvVar) } if strings.TrimSpace(cfg.ResponseSigner.PrivateKeyPEMPath) == "" { return Config{}, fmt.Errorf("load gateway config: %s must not be empty", responseSignerPrivateKeyPEMPathEnvVar) } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.PublicAuth, publicAuthMaxBodyBytesEnvVar, publicAuthRateLimitRequestsEnvVar, publicAuthRateLimitWindowEnvVar, publicAuthRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.BrowserBootstrap, browserBootstrapMaxBodyBytesEnvVar, browserBootstrapRateLimitRequestsEnvVar, browserBootstrapRateLimitWindowEnvVar, browserBootstrapRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.BrowserAsset, browserAssetMaxBodyBytesEnvVar, browserAssetRateLimitRequestsEnvVar, browserAssetRateLimitWindowEnvVar, browserAssetRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicRoutePolicyConfig(cfg.PublicHTTP.AntiAbuse.PublicMisc, publicMiscMaxBodyBytesEnvVar, publicMiscRateLimitRequestsEnvVar, publicMiscRateLimitWindowEnvVar, publicMiscRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicAuthIdentityPolicyConfig(cfg.PublicHTTP.AntiAbuse.SendEmailCodeIdentity, sendEmailCodeIdentityRateLimitRequestsEnvVar, sendEmailCodeIdentityRateLimitWindowEnvVar, sendEmailCodeIdentityRateLimitBurstEnvVar); err != nil { return Config{}, err } if err := validatePublicAuthIdentityPolicyConfig(cfg.PublicHTTP.AntiAbuse.ConfirmEmailCodeIdentity, confirmEmailCodeIdentityRateLimitRequestsEnvVar, confirmEmailCodeIdentityRateLimitWindowEnvVar, confirmEmailCodeIdentityRateLimitBurstEnvVar); err != nil { return Config{}, err } return cfg, nil } func loadPublicRoutePolicyConfigFromEnv(defaults PublicRoutePolicyConfig, maxBodyEnvVar string, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (PublicRoutePolicyConfig, error) { policy := defaults maxBodyBytes, err := loadInt64EnvWithDefault(maxBodyEnvVar, defaults.MaxBodyBytes) if err != nil { return PublicRoutePolicyConfig{}, err } policy.MaxBodyBytes = maxBodyBytes rateLimit, err := loadRateLimitConfigFromEnv(defaults.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) if err != nil { return PublicRoutePolicyConfig{}, err } policy.RateLimit = rateLimit return policy, nil } func loadPublicAuthIdentityPolicyConfigFromEnv(defaults PublicAuthIdentityPolicyConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (PublicAuthIdentityPolicyConfig, error) { rateLimit, err := loadRateLimitConfigFromEnv(defaults.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) if err != nil { return PublicAuthIdentityPolicyConfig{}, err } return PublicAuthIdentityPolicyConfig{RateLimit: rateLimit}, nil } func loadRateLimitConfigFromEnv(defaults RateLimitConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) (RateLimitConfig, error) { cfg := defaults requests, err := loadIntEnvWithDefault(requestsEnvVar, defaults.Requests) if err != nil { return RateLimitConfig{}, err } cfg.Requests = requests window, err := loadDurationEnvWithDefault(windowEnvVar, defaults.Window) if err != nil { return RateLimitConfig{}, err } cfg.Window = window burst, err := loadIntEnvWithDefault(burstEnvVar, defaults.Burst) if err != nil { return RateLimitConfig{}, err } cfg.Burst = burst return cfg, nil } func validateLogLevel(level string) error { switch strings.ToLower(strings.TrimSpace(level)) { case "debug", "info", "warn", "error", "dpanic", "panic", "fatal": return nil default: return fmt.Errorf("%s must be one of debug, info, warn, error, dpanic, panic, fatal", logLevelEnvVar) } } func loadIntEnvWithDefault(envVar string, fallback int) (int, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := strconv.Atoi(rawValue) if err != nil { return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func loadInt64EnvWithDefault(envVar string, fallback int64) (int64, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := strconv.ParseInt(rawValue, 10, 64) if err != nil { return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func loadDurationEnvWithDefault(envVar string, fallback time.Duration) (time.Duration, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := time.ParseDuration(rawValue) if err != nil { return 0, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func loadBoolEnvWithDefault(envVar string, fallback bool) (bool, error) { rawValue, ok := os.LookupEnv(envVar) if !ok { return fallback, nil } value, err := strconv.ParseBool(rawValue) if err != nil { return false, fmt.Errorf("load gateway config: parse %s: %w", envVar, err) } return value, nil } func validatePublicRoutePolicyConfig(cfg PublicRoutePolicyConfig, maxBodyEnvVar string, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error { if cfg.MaxBodyBytes < 0 { return fmt.Errorf("load gateway config: %s must not be negative", maxBodyEnvVar) } return validateRateLimitConfig(cfg.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) } func validatePublicAuthIdentityPolicyConfig(cfg PublicAuthIdentityPolicyConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error { return validateRateLimitConfig(cfg.RateLimit, requestsEnvVar, windowEnvVar, burstEnvVar) } func validateRateLimitConfig(cfg RateLimitConfig, requestsEnvVar string, windowEnvVar string, burstEnvVar string) error { if cfg.Requests <= 0 { return fmt.Errorf("load gateway config: %s must be positive", requestsEnvVar) } if cfg.Window <= 0 { return fmt.Errorf("load gateway config: %s must be positive", windowEnvVar) } if cfg.Burst <= 0 { return fmt.Errorf("load gateway config: %s must be positive", burstEnvVar) } return nil }