Files
Gary HansenandClaude Fable 5 beb595442f feat(receiver): ingest server and admin dashboard
POST /webhook (bearer-token auth, strict validation, 1MB cap) and
/healthz stay open; everything under /admin requires basic auth
(RECEIVER_ADMIN_USER/PASSWORD, refuses to start without a password,
constant-time compares). Admin JSON APIs for the traversal log
(filters, pagination) and stats, plus an embedded dashboard: stat
cards, four Chart.js charts (lazy CDN load with graceful degradation),
filterable log with expandable summaries, auto-refresh. Fetches
resolve against location.origin so credentialed bookmark URLs work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:43:51 +10:00

87 lines
2.4 KiB
Go

// Command exploredns-receiver stores usage webhooks posted by the
// ExploreDNS API server in MySQL or SQLite.
//
// Configuration (environment):
//
// RECEIVER_ADDR listen address (default ":8080")
// RECEIVER_MYSQL_DSN go-sql-driver DSN; when set, events go to MySQL
// RECEIVER_SQLITE_PATH SQLite fallback path (default "data/exploredns-receiver.db")
// RECEIVER_INGEST_TOKEN bearer token required on POST /webhook (open when unset)
// RECEIVER_ADMIN_USER basic-auth username for /admin (default "admin")
// RECEIVER_ADMIN_PASSWORD basic-auth password for /admin (required)
package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"gitea.hansenits.com.au/hits/ExploreDNS/internal/receiver/server"
"gitea.hansenits.com.au/hits/ExploreDNS/internal/receiver/store"
)
// version is stamped at build time via -ldflags "-X main.version=...".
var version = "dev"
func main() {
addr := envOr("RECEIVER_ADDR", ":8080")
adminPass := os.Getenv("RECEIVER_ADMIN_PASSWORD")
if adminPass == "" {
fmt.Fprintln(os.Stderr, "Error: RECEIVER_ADMIN_PASSWORD is not set; refusing to start with an unprotected admin interface")
os.Exit(1)
}
var (
st *store.Store
backend string
err error
)
if dsn := os.Getenv("RECEIVER_MYSQL_DSN"); dsn != "" {
st, err = store.OpenMySQL(dsn)
backend = "mysql " + store.RedactMySQLDSN(dsn)
} else {
path := envOr("RECEIVER_SQLITE_PATH", "data/exploredns-receiver.db")
st, err = store.OpenSQLite(path)
backend = "sqlite " + path
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
srv := server.New(addr, st)
srv.SetVersion(version)
srv.SetIngestToken(os.Getenv("RECEIVER_INGEST_TOKEN"))
srv.SetAdminAuth(envOr("RECEIVER_ADMIN_USER", "admin"), adminPass)
if err := srv.Start(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
log.Printf("ExploreDNS receiver %s listening on %s, storing to %s", version, srv.Addr(), backend)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down...")
if err := srv.Shutdown(15 * time.Second); err != nil {
log.Printf("Shutdown error: %v", err)
}
if err := st.Close(); err != nil {
log.Printf("Close store error: %v", err)
}
}
// envOr returns the environment variable name, or def when unset or empty.
func envOr(name, def string) string {
if v := os.Getenv(name); v != "" {
return v
}
return def
}