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>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e8f56a1ef4
commit
beb595442f
@@ -0,0 +1,86 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user