panel: server status page, per-domain DNS checks, /domains move
Phase 13. Two new packages and one new screen. internal/health owns the shared status vocabulary (ok/warn/error/unknown) and the local checks: supervisord's process table, TLS certificate expiry and the two milter sockets. Each check reports a problem as a status rather than an error, so one broken component costs a line and not the page. internal/dnscheck does the read-only lookups: forward-confirmed reverse DNS for SELFPOST_HOSTNAME, and per-domain DKIM (compared against the key this server actually signs with), SPF and DMARC. Every check is bounded by a timeout and cached, and the resolver sits behind an interface so the tests drive every branch without touching the network. The SPF check is deliberately shallow: it looks for a mechanism literally covering the server's address and does not follow include:/redirect=, so a record that authorises us through an include is reported as "cannot tell" rather than as a failure. /status renders both, with the local checks in an HTMX-polled fragment and the DNS lookups behind a Re-check button, and becomes the panel's landing page: / now redirects there and the domain list lives at /domains. The Reload button moves onto /status, where it reads as what it is — a drift-recovery for the daemons — with text explaining what it regenerates. A template test fails on any remaining href="/" so a stale link cannot silently land on the wrong screen. Also fixes a defect this made visible: the panel could never read the mail queue in the documented deployment. postqueue relies on its setgid-postdrop bit, which the compose file's no-new-privileges disables, so the Queue screen always said "Could not read the mail queue" — including in the released 1.0.0 image. The panel user is now a real member of postdrop, which needs no setgid transition. Verified in a container on the dev server against real DNS: PTR matching (selfpost.mixfed.ru) and not matching (mixfed.ru), DKIM absent and mismatched, SPF absent and via include:, DMARC p=quarantine/p=reject/absent, and a resolver timeout degrading to "unknown" without hanging the page. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+32
-1
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"codeberg.org/mix/selfpost/internal/app"
|
||||
"codeberg.org/mix/selfpost/internal/dnscheck"
|
||||
"codeberg.org/mix/selfpost/internal/domain"
|
||||
"codeberg.org/mix/selfpost/internal/store"
|
||||
)
|
||||
@@ -49,6 +50,15 @@ type Config struct {
|
||||
// honoured, so the header can't be spoofed by anyone but a trusted proxy.
|
||||
// Empty (the default) keeps rate-limiting keyed on RemoteAddr only.
|
||||
TrustedProxyCIDRs []*net.IPNet
|
||||
// TLSCertFile is the certificate Postfix serves on 465/587 (spec 8), read
|
||||
// read-only by the status page to report how much validity is left.
|
||||
TLSCertFile string
|
||||
// OpenDKIMSocket and JournalSocket are the two milter sockets Postfix
|
||||
// connects to. The status page stats them: the first is required for mail
|
||||
// to leave at all (OpenDKIM runs with default_action=tempfail), the second
|
||||
// only for the send log (the journal-milter fails open).
|
||||
OpenDKIMSocket string
|
||||
JournalSocket string
|
||||
}
|
||||
|
||||
// Server is the panel HTTP application.
|
||||
@@ -60,6 +70,7 @@ type Server struct {
|
||||
tmpl *templates
|
||||
sessions *sessionStore
|
||||
setup *setupManager
|
||||
dns *dnscheck.Checker
|
||||
|
||||
loginLimiter *rateLimiter
|
||||
setupLimiter *rateLimiter
|
||||
@@ -83,6 +94,10 @@ func New(st *store.Store, domains *domain.Service, apps *app.Service, cfg Config
|
||||
cfg: cfg,
|
||||
tmpl: tmpl,
|
||||
sessions: newSessionStore(),
|
||||
// Published-DNS checks for the status page and the domain pages. The
|
||||
// checker caches its own results, so page views do not each pay for a
|
||||
// round of lookups (phase 13).
|
||||
dns: dnscheck.New(),
|
||||
// Setup: a handful of attempts per minute per IP is plenty for a
|
||||
// legitimate admin and blunts automated probing (spec 7.6.1).
|
||||
setupLimiter: newRateLimiter(10, time.Minute),
|
||||
@@ -122,10 +137,20 @@ func (s *Server) Handler() http.Handler {
|
||||
// Authenticated panel. Everything not matched by a more specific pattern
|
||||
// above falls through to this sub-mux, wrapped once in the auth middleware.
|
||||
authed := http.NewServeMux()
|
||||
authed.HandleFunc("GET /{$}", s.handleDashboard)
|
||||
|
||||
// The landing page is the server status (phase 13.C): the first thing an
|
||||
// administrator should see after logging in is whether the service is
|
||||
// healthy, not the domain list. handleLogin still redirects to "/".
|
||||
authed.HandleFunc("GET /{$}", redirectToStatus)
|
||||
authed.HandleFunc("GET /status", s.handleStatus)
|
||||
authed.HandleFunc("GET /status/fragment", s.handleStatusFragment)
|
||||
authed.HandleFunc("POST /status/recheck", s.handleStatusRecheck)
|
||||
|
||||
authed.HandleFunc("GET /domains", s.handleDashboard)
|
||||
authed.HandleFunc("POST /domains", s.handleAddDomain)
|
||||
authed.HandleFunc("POST /domains/import", s.handleImportDomain)
|
||||
authed.HandleFunc("GET /domains/{id}", s.handleDomainDetail)
|
||||
authed.HandleFunc("POST /domains/{id}/dns-recheck", s.handleDomainDNSRecheck)
|
||||
authed.HandleFunc("GET /domains/{id}/delete", s.handleDeleteConfirm)
|
||||
authed.HandleFunc("POST /domains/{id}/delete", s.handleDeleteDomain)
|
||||
authed.HandleFunc("POST /domains/{id}/applications", s.handleAddApplication)
|
||||
@@ -159,6 +184,12 @@ func (s *Server) Handler() http.Handler {
|
||||
return mux
|
||||
}
|
||||
|
||||
// redirectToStatus points the panel root at the status page, so there is one
|
||||
// canonical URL for that content instead of two (phase 13.C).
|
||||
func redirectToStatus(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/status", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func handleHealth(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
Reference in New Issue
Block a user