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>
87 lines
2.4 KiB
Go
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
|
|
}
|