Ctrl K
ComunidadGuías…

config/config.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package config

import (
	"flag"
	"fmt"
	"log"
	"math"
	"net"
	"net/url"
	"os"
	"strconv"
	"strings"
	"xprem/internal/helpers"

	"github.com/joho/godotenv"
)

func validateStorageMode(storageMode string) bool {
	return storageMode == "local" || storageMode == "s3" || storageMode == "gcs" || storageMode == "azure"
}

func GetPort() string {
	port := GetEnv("PORT")
	if port == "" {
		port = "3000"
	}
	return port
}

// GetBindAddress returns the address the API server listens on
// (BIND_TO_ADDRESS). The default 0.0.0.0 accepts traffic on every interface.
// Set it to 127.0.0.1 to keep the server on the
// loopback interface, for example behind a reverse proxy on the same host.
func GetBindAddress() string {
	bind_to_address := GetEnv("BIND_TO_ADDRESS")

	bind_address := net.ParseIP(bind_to_address)

	if bind_address == nil {
		log.Fatalf("Invalid BIND_TO_ADDRESS set; must be a valid IP")
	}

	return bind_address.String()
}

func GetDBURL() string {
	return GetEnv("DB_URL")
}

func IsDBMode() bool {
	return GetDBURL() != ""
}

// GetClickHouseURL returns the ClickHouse DSN the Observe feature persists
// telemetry through (e.g. clickhouse://user:password@host:9000/database).
// Empty means Observe (identity included) is not enabled.
func GetClickHouseURL() string {
	return GetEnv("CLICKHOUSE_URL")
}

// IsDeviceTelemetryDisabled reports whether the operator turned off every
// recording made about a device: the manifest check-ins that populate the
// Postgres registry (device_identity and the update-health tables it feeds),
// the identity ops and the telemetry rows the Observe SDK dispatches, and the
// ClickHouse connection those rows would land in. It is the privacy switch a
// deployment flips when it wants to serve updates and collect nothing about
// who receives them.
//
// It disables COLLECTION, not the schema: the tables stay, and whatever was
// recorded before the switch stays with them. Deleting it is the operator's
// call, not a boot side effect.
func IsDeviceTelemetryDisabled() bool {
	disabled, _ := strconv.ParseBool(GetEnv("DISABLE_DEVICE_TELEMETRY"))
	return disabled
}

func IsServerTelemetryDisabled() bool {
	disabled, _ := strconv.ParseBool(GetEnv("DISABLE_TELEMETRY"))
	return disabled
}

func ValidateMasterKey() error {
	awsKeyId := GetEnv("AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID")
	localKey := GetEnv("DB_KEYS_MASTER_KEY_B64")
	if awsKeyId == "" && localKey == "" {
		return fmt.Errorf("Neither AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID nor DB_KEYS_MASTER_KEY_B64 is set: DB mode requires a master key to seal the per-app Expo signing keys stored in Postgres. Generate one with `openssl rand -base64 32`")
	}
	if awsKeyId != "" && localKey != "" {
		log.Printf("Both AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID and DB_KEYS_MASTER_KEY_B64 are set; please set only one")
		return fmt.Errorf("Both AWSSM_DB_KEYS_MASTER_KEY_SECRET_ID and DB_KEYS_MASTER_KEY_B64 are set; please set only one")
	}
	return nil
}

func validateBucketParams(storageMode string) bool {
	switch storageMode {
	case "s3":
		bucketName := GetEnv("S3_BUCKET_NAME")
		if bucketName == "" {
			log.Printf("S3_BUCKET_NAME not set")
			return false
		}
		region := GetEnv("AWS_REGION")
		if region == "" {
			log.Printf("AWS_REGION not set")
			return false
		}
	case "gcs":
		bucketName := GetEnv("GCS_BUCKET_NAME")
		if bucketName == "" {
			log.Printf("GCS_BUCKET_NAME not set")
			return false
		}
	case "azure":
		if GetEnv("AZURE_BLOB_CONTAINER_NAME") == "" {
			log.Printf("AZURE_BLOB_CONTAINER_NAME not set")
			return false
		}
		if GetEnv("AZURE_STORAGE_ACCOUNT_NAME") == "" {
			log.Printf("AZURE_STORAGE_ACCOUNT_NAME not set")
			return false
		}
		// The account key is required in every case: shared key auth is the
		// only supported mode and SAS URLs cannot be signed without it.
		if GetEnv("AZURE_STORAGE_ACCOUNT_KEY") == "" {
			log.Printf("AZURE_STORAGE_ACCOUNT_KEY not set")
			return false
		}
	case "local":
		// Already handled by default values
		return true
	default:
		return false
	}
	return true
}

func validateBaseUrl(baseUrl string) bool {
	if baseUrl == "" || !helpers.IsValidURL(baseUrl) {
		return false
	}
	parsed, err := url.Parse(baseUrl)
	if err != nil {
		return false
	}
	// Paths are appended to BASE_URL, so a query or fragment would end up
	// before them.
	return parsed.RawQuery == "" && !parsed.ForceQuery && parsed.Fragment == ""
}

// BaseURL is BASE_URL without any trailing slash.
func BaseURL() string {
	return strings.TrimRight(GetEnv("BASE_URL"), "/")
}

// ServeFromSubPath reports whether the server itself serves under BASE_URL's
// path; when false the reverse proxy is expected to strip the prefix.
func ServeFromSubPath() bool {
	return GetEnv("SERVE_FROM_SUB_PATH") == "true"
}

// PublicPath is the path component of BASE_URL, empty when BASE_URL has none.
func PublicPath() string {
	parsed, err := url.Parse(BaseURL())
	if err != nil {
		return ""
	}
	p := strings.TrimRight(parsed.Path, "/")
	if p == "" || p == "/" {
		return ""
	}
	if !strings.HasPrefix(p, "/") {
		return "/" + p
	}
	return p
}

// PublicHref is path as a root-relative URL under BASE_URL.
func PublicHref(path string) string {
	if path == "" {
		path = "/"
	}
	if !strings.HasPrefix(path, "/") {
		path = "/" + path
	}
	return PublicPath() + path
}

func IsTestMode() bool {
	return flag.Lookup("test.v") != nil
}

func resolveDefaultBaseUrl() string {
	port := os.Getenv("PORT")
	if port == "" {
		return "http://localhost:3000"
	}
	return "http://localhost:" + port
}

func LoadConfig() {
	err := godotenv.Load()
	if err != nil {
		log.Printf("No .env file found, continuing with runtime environment variables.")
	}
	storageMode := GetEnv("STORAGE_MODE")
	if !validateStorageMode(storageMode) {
		log.Fatalf("Invalid STORAGE_MODE: %s", storageMode)
	}
	bucketParamsValid := validateBucketParams(storageMode)
	if !bucketParamsValid {
		log.Fatalf("Invalid bucket parameters")
	}
	baseUrl := GetEnv("BASE_URL")
	if !validateBaseUrl(baseUrl) {
		log.Fatalf("Invalid BASE_URL: %s", baseUrl)
	}
	jwtSecret := GetEnv("JWT_SECRET")
	if jwtSecret == "" {
		log.Fatalf("JWT_SECRET not set")
	}
	if _, err := parseBundleDiffingMaxBundleSize(); err != nil {
		log.Fatalf("Invalid BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB: %v", err)
	}
	if _, err := parseBundleDiffingPatchMaxRatio(); err != nil {
		log.Fatalf("Invalid BUNDLE_DIFFING_PATCH_MAX_RATIO: %v", err)
	}
	for _, key := range []string{"OBSERVE_INGEST_LIMIT_PER_IP", "OBSERVE_INGEST_LIMIT_PER_APP"} {
		if _, err := parseLimit(GetEnv(key)); err != nil {
			log.Fatalf("Invalid %s: %v", key, err)
		}
	}
}

// IsSourcemapUploadEnabled reports whether publishes store the bundle's source
// map alongside the update (UPLOAD_SOURCEMAPS=true, off by default).
func IsSourcemapUploadEnabled() bool {
	enabled, _ := strconv.ParseBool(GetEnv("UPLOAD_SOURCEMAPS"))
	return enabled && IsDBMode()
}

// IsBundleDiffingEnabled reports whether bundle patches are computed at
// publish and served to devices (BUNDLE_DIFFING=true, off by default).
func IsBundleDiffingEnabled() bool {
	enabled, _ := strconv.ParseBool(GetEnv("BUNDLE_DIFFING"))
	return enabled && IsDBMode()
}

// IsBundleDiffingCDNRedirect reports whether patch requests are redirected to
// the CDN instead of served by this server (BUNDLE_DIFFING_CDN_REDIRECT=true).
// The operator asserts that the CDN edge adds the im and expo-base-update-id
// response headers on the bsdiff objects; nothing here can check it.
func IsBundleDiffingCDNRedirect() bool {
	enabled, _ := strconv.ParseBool(GetEnv("BUNDLE_DIFFING_CDN_REDIRECT"))
	return enabled
}

// IsPreventCDNRedirectionHeaderEnabled reports whether clients may force asset
// delivery through the server with prevent-cdn-redirection: true. It is off by
// default because enabling it lets any asset client bypass the configured CDN.
func IsPreventCDNRedirectionHeaderEnabled() bool {
	enabled, _ := strconv.ParseBool(GetEnv("ENABLE_PREVENT_CDN_REDIRECTION_HEADER"))
	return enabled
}

// BundleDiffingMaxBundleSize is the largest launch asset, in bytes, a patch
// job loads in memory (BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB, default 64). A job
// peaks at about six times this size, and two jobs run at once.
func BundleDiffingMaxBundleSize() int64 {
	size, err := parseBundleDiffingMaxBundleSize()
	if err != nil {
		size, _ = parseMB(DefaultEnvValues["BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB"])
	}
	return size
}

// BundleDiffingPatchMaxRatio is the largest patch worth storing, as a fraction
// of the gzipped bundle a full download would cost
// (BUNDLE_DIFFING_PATCH_MAX_RATIO in (0, 1], default 0.3).
func BundleDiffingPatchMaxRatio() float64 {
	ratio, err := parseBundleDiffingPatchMaxRatio()
	if err != nil {
		ratio, _ = parseRatio(DefaultEnvValues["BUNDLE_DIFFING_PATCH_MAX_RATIO"])
	}
	return ratio
}

func parseBundleDiffingMaxBundleSize() (int64, error) {
	return parseMB(GetEnv("BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB"))
}

func parseBundleDiffingPatchMaxRatio() (float64, error) {
	return parseRatio(GetEnv("BUNDLE_DIFFING_PATCH_MAX_RATIO"))
}

// ObserveIngestLimitPerIP is how many telemetry batches one address may send
// to one app per minute (OBSERVE_INGEST_LIMIT_PER_IP, default 120, 0 disables).
func ObserveIngestLimitPerIP() int {
	return limitOrDefault("OBSERVE_INGEST_LIMIT_PER_IP")
}

// ObserveIngestLimitPerApp is how many telemetry batches one app may receive
// per minute (OBSERVE_INGEST_LIMIT_PER_APP, default 6000, 0 disables).
func ObserveIngestLimitPerApp() int {
	return limitOrDefault("OBSERVE_INGEST_LIMIT_PER_APP")
}

func limitOrDefault(key string) int {
	limit, err := parseLimit(GetEnv(key))
	if err != nil {
		limit, _ = parseLimit(DefaultEnvValues[key])
	}
	return limit
}

func parseLimit(value string) (int, error) {
	limit, err := strconv.Atoi(value)
	if err != nil {
		return 0, err
	}
	if limit < 0 {
		return 0, fmt.Errorf("%d is negative", limit)
	}
	return limit, nil
}

func parseMB(value string) (int64, error) {
	mb, err := strconv.ParseInt(value, 10, 64)
	if err != nil {
		return 0, err
	}
	if mb <= 0 || mb > math.MaxInt64>>20 {
		return 0, fmt.Errorf("%d is not a usable number of megabytes", mb)
	}
	return mb << 20, nil
}

func parseRatio(value string) (float64, error) {
	ratio, err := strconv.ParseFloat(value, 64)
	if err != nil {
		return 0, err
	}
	if math.IsNaN(ratio) || ratio <= 0 || ratio > 1 {
		return 0, fmt.Errorf("%v is not in (0, 1]", ratio)
	}
	return ratio, nil
}

var DefaultEnvValues = map[string]string{
	"LOCAL_BUCKET_BASE_PATH": "./updates",
	"STORAGE_MODE":           "local",
	"BASE_URL":               resolveDefaultBaseUrl(),
	"JWT_SECRET":             "",
	"AWS_REGION":             "eu-west-3",
	"AWS_BASE_ENDPOINT":      "",

	// Audit archive (ee/audit): opt-in periodic NDJSON export of the audit
	// log to a DEDICATED bucket/container/directory (per-provider name
	// variables, see ee/audit). Off by default:
	// writing to the operator's storage must be a choice.
	"ARCHIVE_AUDIT_LOGS":                 "false",
	"AUDIT_LOGS_EXPORT_INTERVAL_SECONDS": "300",
	"LOCAL_AUDIT_LOGS_BASE_PATH":         "./audit-logs",

	// Audit log retention (ee/audit): about 1.5 years, matching EAS and the
	// 1-3 year industry norm. Purged rows are gone from Postgres; anything
	// longer lived belongs to the operator's own pipeline (database backups,
	// the audit stream once enabled).
	"AUDIT_LOG_RETENTION_DAYS": "550",

	// Bundle diffing (bsdiff patches between updates): off by default. Patches
	// are served by this server, not the CDN, so a patch is only kept when it
	// beats the gzipped bundle by a wide margin.
	"BUNDLE_DIFFING":                    "false",
	"BUNDLE_DIFFING_CDN_REDIRECT":       "false",
	"BUNDLE_DIFFING_MAX_BUNDLE_SIZE_MB": "64",
	"BUNDLE_DIFFING_PATCH_MAX_RATIO":    "0.3",

	// Source map uploads: off by default. The maps land in their own store,
	// which may be the updates one when it is not publicly readable: a map
	// embeds the app's source code.
	"UPLOAD_SOURCEMAPS":          "false",
	"LOCAL_SOURCEMAPS_BASE_PATH": "./sourcemaps",

	// Client-controlled CDN bypass: opt-in because every asset client can use
	// the header once it is enabled.
	"ENABLE_PREVENT_CDN_REDIRECTION_HEADER": "false",

	// Telemetry ingestion budgets, in batches per minute. The SDK sends a few
	// batches each time the app goes to the background, so the per-app default
	// leaves room for about 100K monthly active users.
	"OBSERVE_INGEST_LIMIT_PER_IP":  "120",
	"OBSERVE_INGEST_LIMIT_PER_APP": "6000",

	// Default address to bind to
	"BIND_TO_ADDRESS": "0.0.0.0",

	// Database connection defaults
	"DB_URL":                "",
	"DB_MAX_CONNS":          "25",
	"DB_MIN_CONNS":          "5",
	"DB_MAX_CONN_LIFETIME":  "30m",
	"DB_MAX_CONN_IDLE_TIME": "5m",
}

func GetEnv(key string) string {
	value := os.Getenv(key)
	if value == "" {
		defaultValue := DefaultEnvValues[key]
		if defaultValue != "" {
			return defaultValue
		}
		return ""
	}
	return value
}