package handlers import ( "net/http" "net/netip" "strconv" "strings" "sneak.berlin/go/webhooker/internal/config" ) // notSet is what the Settings page shows for a value that is empty. const notSet = "not set" // settingRow is one line of the Settings page: an environment // variable, what it controls, and the value the server loaded for it. type settingRow struct { Name string Description string Value string } // HandleSettings returns a handler for the read-only Settings page, // which lists the configuration the server started with. func (h *Handlers) HandleSettings() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { h.renderTemplate(w, r, "settings.html", map[string]any{ "Settings": settingRows(h.params.Config), }) } } // settingRows lists every field of cfg under the environment variable // it is read from, in the order of the README's configuration table. // METRICS_PASSWORD and SENTRY_DSN are credentials, so their values // never reach the page: only whether they are set. func settingRows(cfg *config.Config) []settingRow { metricsUsername := cfg.MetricsUsername if metricsUsername == "" { metricsUsername = notSet } return []settingRow{ {"WEBHOOKER_ENVIRONMENT", "dev or prod", cfg.Environment}, {"PORT", "HTTP listen port", strconv.Itoa(cfg.Port)}, { "BIND_ADDRESS", "IP address the HTTP listener binds", cfg.BindAddress, }, { "DATA_DIR", "Directory for all SQLite databases", cfg.DataDir, }, { "DEBUG", "Enable debug logging", strconv.FormatBool(cfg.Debug), }, { "MAINTENANCE_MODE", "Report maintenanceMode: true in the healthcheck JSON. " + "It does not change how any request is served", strconv.FormatBool(cfg.MaintenanceMode), }, { "METRICS_USERNAME", "Basic auth username for /metrics", metricsUsername, }, { "METRICS_PASSWORD", "Basic auth password for /metrics", setOrNotSet(cfg.MetricsPassword), }, { "SENTRY_DSN", "Error reporting DSN. Unset leaves error reporting off", setOrNotSet(cfg.SentryDSN), }, { "RETENTION_SWEEP_INTERVAL", "How often the retention reaper and archive sweeper run", cfg.RetentionSweepInterval.String(), }, { "SESSION_IDLE_TIMEOUT", "Idle session timeout. Zero or negative disables idle " + "expiry", cfg.SessionIdleTimeout.String(), }, { "RECEIVER_RATE_LIMIT", "Receiver requests per minute per IP per entrypoint " + "(10x that per IP across the route)", strconv.Itoa(cfg.ReceiverRateLimit), }, { "TRUSTED_PROXIES", "CIDRs whose forwarded headers are trusted", cidrList(cfg.TrustedProxies), }, { "ALLOWED_EGRESS_CIDRS", "CIDRs that delivery targets may reach despite the " + "SSRF blocklist", cidrList(cfg.AllowedEgressCIDRs), }, } } // setOrNotSet is how the Settings page shows a credential: whether it // has a value, never the value itself. func setOrNotSet(value string) string { if value == "" { return notSet } return "set" } // cidrList renders a CIDR list setting for the Settings page. func cidrList(prefixes []netip.Prefix) string { if len(prefixes) == 0 { return "none" } return strings.Join(config.PrefixStrings(prefixes), ", ") }