Files
webhooker/internal/handlers/settings.go
T
clawbot 4958a6f2e4
check / check (push) Waiting to run
Isolate config tests from the shell; any out-of-range PORT is ErrInvalidPort (closes #94)
The config tests unset variables without restoring them and read whatever the developer's shell exported, so results could differ from one machine to the next. config.ClearEnvForTest, in internal/config/testing.go, unsets every variable in the process environment and, when the test ends, leaves it exactly as it found it; every config test and the first-boot test call it before setting their own. TestEnvPositiveInt and TestEnvPort share one table runner, and the port errors name the bad value. A PORT of zero, below zero, or too large to parse now matches ErrInvalidPort, as one above 65535 already did. The README and the Settings page say an unparseable or non-positive RETENTION_SWEEP_INTERVAL stops startup.

Model: opus-5-5
2026-10-02 17:44:30 +02:00

130 lines
3.8 KiB
Go

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, with the description the README's configuration
// table gives it (less its pointers to other README sections), in the
// table's order. 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. Loopback by default, " +
"so the cleartext listener is not published on every " +
"interface. The Docker image ships 0.0.0.0 instead",
cfg.BindAddress,
},
{"DATA_DIR", "Directory for all SQLite databases", cfg.DataDir},
{"DEBUG", "Enable debug logging", strconv.FormatBool(cfg.Debug)},
{
"METRICS_USERNAME",
"Basic auth username for /metrics. Must be set together " +
"with METRICS_PASSWORD; one without the other fails " +
"startup",
metricsUsername,
},
{
"METRICS_PASSWORD",
"Basic auth password for /metrics. Must be set together " +
"with METRICS_USERNAME; one without the other fails " +
"startup",
setOrNotSet(cfg.MetricsPassword),
},
{
"SENTRY_DSN",
"Sentry error reporting DSN. Unset leaves error reporting " +
"off; a value the Sentry SDK cannot parse fails startup " +
"rather than serving with reporting silently off",
setOrNotSet(cfg.SentryDSN),
},
{
"RETENTION_SWEEP_INTERVAL",
"How often the retention reaper and archive sweeper run " +
"(Go duration, must be positive). A value that does " +
"not parse, or is zero or negative, fails startup",
cfg.RetentionSweepInterval.String(),
},
{
"SESSION_IDLE_TIMEOUT",
"Idle session timeout (Go duration)",
cfg.SessionIdleTimeout.String(),
},
{
"RECEIVER_RATE_LIMIT",
"Receiver requests/minute per IP per entrypoint " +
"(10x that per IP across the route)",
strconv.Itoa(cfg.ReceiverRateLimit),
},
{
"TRUSTED_PROXIES",
"CIDRs whose forwarded headers are trusted. A set value " +
"replaces the default. If any client can reach webhooker, " +
"or the proxy in front of it, from an RFC 1918 source " +
"address, set it to the proxy's address alone",
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), ", ")
}