Files
m 20d361a886
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled
рабочий вариант, но скороть 10 МБит
2026-05-22 21:46:49 +03:00

318 lines
9.8 KiB
Go

package config
import (
"encoding/base64"
"fmt"
"os"
"strconv"
"strings"
"time"
)
type Config struct {
App AppConfig
HTTP HTTPConfig
FabricControl FabricControlConfig
Postgres PostgresConfig
Redis RedisConfig
Auth AuthConfig
Installation InstallationConfig
DataPlane DataPlaneConfig
Secret SecretConfig
Session SessionConfig
Worker WorkerConfig
WebSocket WebSocketConfig
}
type AppConfig struct {
Name string
Env string
}
type HTTPConfig struct {
Host string
Port int
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
ShutdownTimeout time.Duration
}
type FabricControlConfig struct {
Enabled bool
ListenAddr string
}
type PostgresConfig struct {
DSN string
MaxConns int32
MinConns int32
ConnectTimeout time.Duration
}
type RedisConfig struct {
Addr string
Password string
DB int
DialTimeout time.Duration
}
type AuthConfig struct {
AccessTokenTTL time.Duration
RefreshTokenTTL time.Duration
Issuer string
AccessTokenSecret string
RefreshHashSecret string
}
type InstallationConfig struct {
AuthorityMode string
ProductRootPublicKeyBase64 string
ProductRootPublicKeyFile string
AllowInsecureBootstrap bool
}
type DataPlaneConfig struct {
TokenTTL time.Duration
TokenPrivateKeyPEM string
TokenPrivateKeyFile string
BackendGatewayURL string
DirectWorkerWSSURLTemplate string
DirectWorkerJSONRuntime bool
DirectWorkerBinaryRender bool
DirectWorkerTLSTrustMode string
DirectWorkerTLSCARef string
}
type SecretConfig struct {
EncryptionKeyBase64 string
EncryptionKeyFile string
EncryptionKeyID string
}
type SessionConfig struct {
HeartbeatTTL time.Duration
DetachGracePeriod time.Duration
AttachTokenTTL time.Duration
LiveStateTTL time.Duration
RecoveryBatchSize int
}
type WorkerConfig struct {
LeaseTTL time.Duration
HeartbeatTTL time.Duration
StaleLeaseGracePeriod time.Duration
}
type WebSocketConfig struct {
WriteTimeout time.Duration
PingInterval time.Duration
PongWait time.Duration
}
func Load() (Config, error) {
cfg := Config{
App: AppConfig{
Name: getEnv("APP_NAME", "rap-api"),
Env: getEnv("APP_ENV", "development"),
},
HTTP: HTTPConfig{
Host: getEnv("HTTP_HOST", "0.0.0.0"),
Port: getInt("HTTP_PORT", 8080),
ReadTimeout: getDuration("HTTP_READ_TIMEOUT", 15*time.Second),
WriteTimeout: getDuration("HTTP_WRITE_TIMEOUT", 15*time.Second),
IdleTimeout: getDuration("HTTP_IDLE_TIMEOUT", 60*time.Second),
ShutdownTimeout: getDuration("HTTP_SHUTDOWN_TIMEOUT", 10*time.Second),
},
FabricControl: FabricControlConfig{
Enabled: getBool("FABRIC_CONTROL_QUIC_ENABLED", false),
ListenAddr: getEnv("FABRIC_CONTROL_QUIC_LISTEN_ADDR", ":19191"),
},
Postgres: PostgresConfig{
DSN: getEnv("POSTGRES_DSN", ""),
MaxConns: int32(getInt("POSTGRES_MAX_CONNS", 20)),
MinConns: int32(getInt("POSTGRES_MIN_CONNS", 2)),
ConnectTimeout: getDuration("POSTGRES_CONNECT_TIMEOUT", 5*time.Second),
},
Redis: RedisConfig{
Addr: getEnv("REDIS_ADDR", "localhost:6379"),
Password: getEnv("REDIS_PASSWORD", ""),
DB: getInt("REDIS_DB", 0),
DialTimeout: getDuration("REDIS_DIAL_TIMEOUT", 5*time.Second),
},
Auth: AuthConfig{
AccessTokenTTL: getDuration("AUTH_ACCESS_TOKEN_TTL", 15*time.Minute),
RefreshTokenTTL: getDuration("AUTH_REFRESH_TOKEN_TTL", 30*24*time.Hour),
Issuer: getEnv("AUTH_ISSUER", "rap-api"),
AccessTokenSecret: getEnv("AUTH_ACCESS_TOKEN_SECRET", ""),
RefreshHashSecret: getEnv("AUTH_REFRESH_HASH_SECRET", ""),
},
Installation: InstallationConfig{
AuthorityMode: getEnv("INSTALLATION_AUTHORITY_MODE", ""),
ProductRootPublicKeyBase64: getEnv("INSTALLATION_PRODUCT_ROOT_PUBLIC_KEY_B64", ""),
ProductRootPublicKeyFile: getEnv("INSTALLATION_PRODUCT_ROOT_PUBLIC_KEY_FILE", ""),
AllowInsecureBootstrap: getBool("INSTALLATION_INSECURE_BOOTSTRAP_ENABLED", false),
},
DataPlane: DataPlaneConfig{
TokenTTL: getDuration("DATA_PLANE_TOKEN_TTL", 1*time.Minute),
TokenPrivateKeyPEM: getEnv("DATA_PLANE_TOKEN_PRIVATE_KEY_PEM", ""),
TokenPrivateKeyFile: getEnv("DATA_PLANE_TOKEN_PRIVATE_KEY_FILE", ""),
BackendGatewayURL: getEnv("DATA_PLANE_BACKEND_GATEWAY_URL", "/api/v1/gateway/ws"),
DirectWorkerWSSURLTemplate: getEnv("DATA_PLANE_DIRECT_WORKER_WSS_URL_TEMPLATE", ""),
DirectWorkerJSONRuntime: getBool("DATA_PLANE_DIRECT_WORKER_JSON_RUNTIME", false),
DirectWorkerBinaryRender: getBool("DATA_PLANE_DIRECT_WORKER_BINARY_RENDER", false),
DirectWorkerTLSTrustMode: getEnv("DATA_PLANE_DIRECT_WORKER_TLS_TRUST_MODE", "smoke_insecure"),
DirectWorkerTLSCARef: getEnv("DATA_PLANE_DIRECT_WORKER_TLS_CA_REF", ""),
},
Secret: SecretConfig{
EncryptionKeyBase64: getEnv("SECRET_ENCRYPTION_KEY_B64", ""),
EncryptionKeyFile: getEnv("SECRET_ENCRYPTION_KEY_FILE", ""),
EncryptionKeyID: getEnv("SECRET_ENCRYPTION_KEY_ID", "local-v1"),
},
Session: SessionConfig{
HeartbeatTTL: getDuration("SESSION_HEARTBEAT_TTL", 90*time.Second),
DetachGracePeriod: getDuration("SESSION_DETACH_GRACE_PERIOD", 30*time.Minute),
AttachTokenTTL: getDuration("SESSION_ATTACH_TOKEN_TTL", 2*time.Minute),
LiveStateTTL: getDuration("SESSION_LIVE_STATE_TTL", 2*time.Minute),
RecoveryBatchSize: getInt("SESSION_RECOVERY_BATCH_SIZE", 100),
},
Worker: WorkerConfig{
LeaseTTL: getDuration("WORKER_LEASE_TTL", 45*time.Second),
HeartbeatTTL: getDuration("WORKER_HEARTBEAT_TTL", 15*time.Second),
StaleLeaseGracePeriod: getDuration("WORKER_STALE_LEASE_GRACE_PERIOD", 30*time.Second),
},
WebSocket: WebSocketConfig{
WriteTimeout: getDuration("WEBSOCKET_WRITE_TIMEOUT", 10*time.Second),
PingInterval: getDuration("WEBSOCKET_PING_INTERVAL", 20*time.Second),
PongWait: getDuration("WEBSOCKET_PONG_WAIT", 40*time.Second),
},
}
if cfg.Postgres.DSN == "" {
return Config{}, fmt.Errorf("POSTGRES_DSN is required")
}
if cfg.Auth.AccessTokenSecret == "" {
return Config{}, fmt.Errorf("AUTH_ACCESS_TOKEN_SECRET is required")
}
if cfg.Auth.RefreshHashSecret == "" {
return Config{}, fmt.Errorf("AUTH_REFRESH_HASH_SECRET is required")
}
if cfg.Installation.ProductRootPublicKeyBase64 == "" && cfg.Installation.ProductRootPublicKeyFile != "" {
publicKey, err := os.ReadFile(cfg.Installation.ProductRootPublicKeyFile)
if err != nil {
return Config{}, fmt.Errorf("read INSTALLATION_PRODUCT_ROOT_PUBLIC_KEY_FILE: %w", err)
}
cfg.Installation.ProductRootPublicKeyBase64 = strings.TrimSpace(string(publicKey))
}
cfg.Installation.AuthorityMode = normalizeInstallationAuthorityMode(cfg.Installation.AuthorityMode, cfg.Installation.ProductRootPublicKeyBase64)
if isProductionEnv(cfg.App.Env) && cfg.Installation.AuthorityMode != "strict" {
return Config{}, fmt.Errorf("INSTALLATION_AUTHORITY_MODE=strict with INSTALLATION_PRODUCT_ROOT_PUBLIC_KEY_B64 or file is required in production")
}
if cfg.DataPlane.TokenPrivateKeyPEM == "" && cfg.DataPlane.TokenPrivateKeyFile != "" {
privateKey, err := os.ReadFile(cfg.DataPlane.TokenPrivateKeyFile)
if err != nil {
return Config{}, fmt.Errorf("read DATA_PLANE_TOKEN_PRIVATE_KEY_FILE: %w", err)
}
cfg.DataPlane.TokenPrivateKeyPEM = string(privateKey)
}
if cfg.Secret.EncryptionKeyBase64 == "" && cfg.Secret.EncryptionKeyFile != "" {
secretKey, err := os.ReadFile(cfg.Secret.EncryptionKeyFile)
if err != nil {
return Config{}, fmt.Errorf("read SECRET_ENCRYPTION_KEY_FILE: %w", err)
}
cfg.Secret.EncryptionKeyBase64 = strings.TrimSpace(string(secretKey))
}
if cfg.Secret.EncryptionKeyBase64 != "" {
decoded, err := base64.StdEncoding.DecodeString(cfg.Secret.EncryptionKeyBase64)
if err != nil {
if decodedRaw, rawErr := base64.RawStdEncoding.DecodeString(cfg.Secret.EncryptionKeyBase64); rawErr == nil {
decoded = decodedRaw
} else {
return Config{}, fmt.Errorf("SECRET_ENCRYPTION_KEY_B64 must be base64 encoded: %w", err)
}
}
if len(decoded) != 32 {
return Config{}, fmt.Errorf("SECRET_ENCRYPTION_KEY_B64 must decode to 32 bytes for AES-256-GCM")
}
}
if isProductionEnv(cfg.App.Env) && cfg.Secret.EncryptionKeyBase64 == "" {
return Config{}, fmt.Errorf("SECRET_ENCRYPTION_KEY_B64 or SECRET_ENCRYPTION_KEY_FILE is required in production")
}
return cfg, nil
}
func normalizeInstallationAuthorityMode(mode string, rootPublicKey string) string {
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "strict", "compat":
return mode
case "":
if strings.TrimSpace(rootPublicKey) != "" {
return "strict"
}
return "compat"
default:
return mode
}
}
func isProductionEnv(appEnv string) bool {
switch strings.ToLower(strings.TrimSpace(appEnv)) {
case "production", "prod":
return true
default:
return false
}
}
func getEnv(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
func getInt(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return parsed
}
func getBool(key string, fallback bool) bool {
value := os.Getenv(key)
if value == "" {
return fallback
}
switch value {
case "1", "true", "TRUE", "yes", "on":
return true
case "0", "false", "FALSE", "no", "off":
return false
default:
return fallback
}
}
func getDuration(key string, fallback time.Duration) time.Duration {
value := os.Getenv(key)
if value == "" {
return fallback
}
parsed, err := time.ParseDuration(value)
if err != nil {
return fallback
}
return parsed
}