feat: contact form backend for linux and freebsd servers
Test / test (push) Successful in 2m1s
Release / gates (push) Successful in 1m57s
Release / build (amd64, freebsd) (push) Successful in 1m26s
Release / build (amd64, linux) (push) Successful in 1m30s
Release / build (arm64, freebsd) (push) Successful in 1m28s
Release / build (arm64, linux) (push) Successful in 1m49s
Release / build (loong64, linux) (push) Successful in 1m30s
Release / build (riscv64, linux) (push) Successful in 1m29s
Release / release (push) Successful in 41s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-29 00:32:56 +02:00
commit 3a38f00dc0
49 changed files with 10769 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
//go:build linux || freebsd
package handler
import (
"crypto/subtle"
"encoding/json"
"log/slog"
"net/http"
"strings"
"sync"
"sourcedock.dev/petrbalvin/nuntius/internal/config"
)
// Canonical metric names. The same identifiers serve as counter keys in
// bump() and as JSON field names in GET /metrics responses.
const (
metricReceived = "received"
metricHoneypotBlocked = "honeypot_blocked"
metricRateLimited = "rate_limited"
metricOriginBlocked = "origin_blocked"
metricBodyTooLarge = "body_too_large"
metricInvalidBody = "invalid_body"
metricValidationFailed = "validation_failed"
metricSendFailed = "send_failed"
metricPersistFailed = "persist_failed"
metricDuplicateSignup = "duplicate_signup"
metricConfirmationSent = "confirmation_sent"
metricConfirmed = "confirmed"
metricConfirmFailed = "confirmation_failed"
metricSent = "sent"
metricAutoReplyFailed = "auto_reply_failed"
metricTelegramFailed = "telegram_failed"
)
// metricNames lists every counter in stable order so totals and snapshots
// cannot drift from the struct fields.
var metricNames = []string{
metricReceived,
metricHoneypotBlocked,
metricRateLimited,
metricOriginBlocked,
metricBodyTooLarge,
metricInvalidBody,
metricValidationFailed,
metricSendFailed,
metricPersistFailed,
metricDuplicateSignup,
metricConfirmationSent,
metricConfirmed,
metricConfirmFailed,
metricSent,
metricAutoReplyFailed,
metricTelegramFailed,
}
// FormStats holds lifetime counters for one form. Every field counts
// outcomes of requests routed to that form's endpoints.
type FormStats struct {
Received int64 `json:"received"`
HoneypotBlocked int64 `json:"honeypot_blocked"`
RateLimited int64 `json:"rate_limited"`
OriginBlocked int64 `json:"origin_blocked"`
BodyTooLarge int64 `json:"body_too_large"`
InvalidBody int64 `json:"invalid_body"`
ValidationFailed int64 `json:"validation_failed"`
SendFailed int64 `json:"send_failed"`
PersistFailed int64 `json:"persist_failed"`
DuplicateSignup int64 `json:"duplicate_signup"`
ConfirmationSent int64 `json:"confirmation_sent"`
Confirmed int64 `json:"confirmed"`
ConfirmFailed int64 `json:"confirmation_failed"`
Sent int64 `json:"sent"`
AutoReplyFailed int64 `json:"auto_reply_failed"`
TelegramFailed int64 `json:"telegram_failed"`
}
// incByIndex increments the counter at metricNames[i]; indexes outside the
// known set are ignored.
func (f *FormStats) incByIndex(i int) {
switch i {
case 0:
f.Received++
case 1:
f.HoneypotBlocked++
case 2:
f.RateLimited++
case 3:
f.OriginBlocked++
case 4:
f.BodyTooLarge++
case 5:
f.InvalidBody++
case 6:
f.ValidationFailed++
case 7:
f.SendFailed++
case 8:
f.PersistFailed++
case 9:
f.DuplicateSignup++
case 10:
f.ConfirmationSent++
case 11:
f.Confirmed++
case 12:
f.ConfirmFailed++
case 13:
f.Sent++
case 14:
f.AutoReplyFailed++
case 15:
f.TelegramFailed++
}
}
// add sums another snapshot into f.
func (f *FormStats) add(other FormStats) {
for i := range metricNames {
switch i {
case 0:
f.Received += other.Received
case 1:
f.HoneypotBlocked += other.HoneypotBlocked
case 2:
f.RateLimited += other.RateLimited
case 3:
f.OriginBlocked += other.OriginBlocked
case 4:
f.BodyTooLarge += other.BodyTooLarge
case 5:
f.InvalidBody += other.InvalidBody
case 6:
f.ValidationFailed += other.ValidationFailed
case 7:
f.SendFailed += other.SendFailed
case 8:
f.PersistFailed += other.PersistFailed
case 9:
f.DuplicateSignup += other.DuplicateSignup
case 10:
f.ConfirmationSent += other.ConfirmationSent
case 11:
f.Confirmed += other.Confirmed
case 12:
f.ConfirmFailed += other.ConfirmFailed
case 13:
f.Sent += other.Sent
case 14:
f.AutoReplyFailed += other.AutoReplyFailed
case 15:
f.TelegramFailed += other.TelegramFailed
}
}
}
// formStatsRegistry guards the per-form counters shared between request
// goroutines and the /metrics endpoint.
type formStatsRegistry struct {
mu sync.Mutex
stats map[string]*FormStats
}
func newFormStatsRegistry(forms map[string]*config.Form) *formStatsRegistry {
r := &formStatsRegistry{stats: make(map[string]*FormStats, len(forms))}
for path := range forms {
r.stats[path] = &FormStats{}
}
return r
}
// bump increments the named counter for a form. Unknown paths or metrics
// are dropped silently so logging can never fail a request.
func (r *formStatsRegistry) bump(path, metric string) {
if r == nil {
return
}
r.mu.Lock()
defer r.mu.Unlock()
fs, ok := r.stats[path]
if !ok {
return
}
for i, name := range metricNames {
if name == metric {
fs.incByIndex(i)
return
}
}
}
// snapshot returns a copy of every form's counters plus their sum.
func (r *formStatsRegistry) snapshot() (map[string]FormStats, FormStats) {
out := make(map[string]FormStats, len(r.stats))
var total FormStats
r.mu.Lock()
defer r.mu.Unlock()
for path, fs := range r.stats {
out[path] = *fs
total.add(*fs)
}
return out, total
}
// Metrics serves GET /metrics: lifetime counters per form and combined
// totals, as JSON. The endpoint is exempt from rate limiting and sends no
// CORS headers, so third-party pages cannot read submission volumes. With
// server.metrics_token set it requires that token as a bearer credential.
func (h *ContactHandler) Metrics(w http.ResponseWriter, r *http.Request) {
if h.metricsToken != "" {
token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if !ok || subtle.ConstantTimeCompare([]byte(token), []byte(h.metricsToken)) != 1 {
w.Header().Set("WWW-Authenticate", `Bearer realm="nuntius metrics"`)
respondError(w, http.StatusUnauthorized, "unauthorized", "A valid bearer token is required.")
return
}
}
if h.stats == nil {
w.WriteHeader(http.StatusNotFound)
return
}
formSnapshots, total := h.stats.snapshot()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if err := json.NewEncoder(w).Encode(map[string]any{
"totals": total,
"forms": formSnapshots,
}); err != nil {
slog.Error("failed to encode metrics response", "err", err)
}
}
// bump records one occurrence of metric for the given form path.
func (h *ContactHandler) bump(path, metric string) {
h.stats.bump(path, metric)
}