Files
josh 17ec55cb85
CI / Lint + build + test (push) Successful in 1m34s
Release / detect (push) Successful in 4s
Release / build-live-image (push) Has been skipped
Release / bundle (push) Successful in 1m5s
chore: cleanup sprint — dead CSS, dedup helpers, handler refactor
Remove ~126 lines of orphaned CSS from tile slim-down and old detail
layout. Consolidate 4 duplicate duration formatters into shared
elapsed()/fmtElapsed() helpers. Break 160-line Result handler into
focused sub-functions. Implement real Hub.Shutdown() (was a no-op).
Standardize agent error responses to JSON. Replace panic() in router
init with error return. Extract magic numbers as named constants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 20:39:38 -04:00

86 lines
2.8 KiB
Go

// 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 (
"fmt"
"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, error) {
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 {
return nil, fmt.Errorf("extract static assets: %w", 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.HostPage)
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("/runs/{runID}", d.UI.RunPage)
r.Get("/reports/{runID}", d.UI.Report)
r.Get("/register/quick.sh", d.UI.QuickRegisterScript)
r.Get("/events", d.UI.SSE)
return r, nil
}