// Package httpserver assembles the chi router. It lives in its own // package because it depends on both `api` and `orchestrator`, and // those two packages must stay import-independent. package httpserver import ( "io/fs" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "vetting/internal/api" "vetting/internal/web" ) type Deps struct { UI *api.UI Agent *api.Agent LiveDir string // directory containing vmlinuz + initrd.img; "" disables /live AgentAssetDir string // directory containing vetting-agent-linux-amd64; "" disables /assets } func NewRouter(d Deps) http.Handler { r := chi.NewRouter() r.Use(middleware.RealIP) r.Use(middleware.Recoverer) r.Use(middleware.Logger) staticFS, err := fs.Sub(web.Static, "static") if err != nil { panic(err) } r.Handle("/static/*", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS)))) if d.LiveDir != "" { r.Handle("/live/*", http.StripPrefix("/live/", http.FileServer(http.Dir(d.LiveDir)))) } // Host-mode agent binary is served here so the quick-register // one-liner can curl it without the operator pre-staging anything. if d.AgentAssetDir != "" { r.Handle("/assets/*", http.StripPrefix("/assets/", http.FileServer(http.Dir(d.AgentAssetDir)))) } // Agent / PXE endpoints — authenticated per-request by bearer token // or by the unforgeable MAC path parameter. r.Get("/ipxe/{mac}", d.Agent.IPXEScript) r.Route("/api/v1/runs/{id}", func(r chi.Router) { r.Post("/hello", d.Agent.Hello) r.Post("/claim", d.Agent.Claim) r.Post("/heartbeat", d.Agent.Heartbeat) r.Post("/log", d.Agent.Log) r.Post("/result", d.Agent.Result) r.Post("/hold", d.Agent.Hold) r.Post("/sensor", d.Agent.Sensor) }) // Quick-register: the bash one-liner fetched from /register/quick.sh // POSTs here from the target host. LAN-trusted, same threat model // as the browser UI. r.Post("/api/v1/hosts", d.UI.CreateHostJSON) // Host-mode agent heartbeat. Keyed by MAC (no bearer token), same // LAN-trust model as /api/v1/hosts. r.Post("/api/v1/hosts/{mac}/heartbeat", d.UI.Heartbeat) // Browser UI — no auth; bind to loopback or LAN only, or front // with a reverse proxy if you need a password. r.Get("/", d.UI.Dashboard) r.Get("/hosts/new", d.UI.NewHostForm) r.Post("/hosts", d.UI.CreateHost) r.Get("/hosts/{id}", d.UI.HostDetail) r.Post("/hosts/{id}/delete", d.UI.DeleteHost) r.Post("/hosts/{id}/start", d.UI.StartRun) r.Post("/hosts/{id}/cancel", d.UI.CancelRun) r.Post("/hosts/{id}/override-wipe", d.UI.OverrideWipeStorage) r.Get("/reports/{runID}", d.UI.Report) r.Get("/register/quick.sh", d.UI.QuickRegisterScript) r.Get("/events", d.UI.SSE) return r }