check / check (push) Waiting to run
Config tests, and the first-boot debug log test that builds a Config, start from an empty environment: config.ClearEnvForTest unsets every variable the process has and restores each when the test ends, so nothing exported in the developer's shell changes a result. TestEnvPositiveInt and TestEnvPort share one table runner, and TestEnvPort checks that the bad value appears in each error. Every out-of-range PORT now wraps ErrInvalidPort: zero, negatives, above 65535, and numbers too large or too small for an int. The README configuration table and the Settings page say that a RETENTION_SWEEP_INTERVAL that does not parse, or is zero or negative, fails startup. Model: opus-5-5
130 lines
3.8 KiB
Go
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), ", ")
|
|
}
|