check / check (push) Waiting to run
A new page at /settings, linked from the navigation bar and behind the login, lists every configuration field the server loaded at startup: its environment variable, the README table's description, and the value in effect. METRICS_PASSWORD and SENTRY_DSN show only as set or not set; their values are replaced before rendering and never reach the template. The route is GET only and its group is built like the other admin page groups. Tests set the credentials one at a time so each value shown is checked against its own field and a set secret never appears in the page. Model: opus-5-5
136 lines
3.9 KiB
Go
136 lines
3.9 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)},
|
|
{
|
|
"MAINTENANCE_MODE",
|
|
"Report maintenanceMode: true in the healthcheck JSON. " +
|
|
"It does not change how any request is served — no " +
|
|
"maintenance page exists",
|
|
strconv.FormatBool(cfg.MaintenanceMode),
|
|
},
|
|
{
|
|
"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)",
|
|
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), ", ")
|
|
}
|