package api import ( "encoding/json" "log" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/google/uuid" dbpkg "crypto-miner-server/internal/db" "crypto-miner-server/internal/recon" ) type ReconHandler struct { db *dbpkg.Database wsHub *WSHub publicURL func() string canaryHub *ReconCanaryHub } func NewReconHandler(database *dbpkg.Database, hub *WSHub, publicURL ...func() string) *ReconHandler { var fn func() string if len(publicURL) > 0 { fn = publicURL[0] } return &ReconHandler{db: database, wsHub: hub, publicURL: fn, canaryHub: NewReconCanaryHub()} } func (h *ReconHandler) Scan(w http.ResponseWriter, r *http.Request) { var req recon.ScanRequest 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 scanID := uuid.New().String() if req.SSRFCanary { h.registerSSRfCanary(scanID, host, r) } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusAccepted) _ = json.NewEncoder(w).Encode(map[string]interface{}{ "scan_id": scanID, "status": "running", }) go h.runScan(req, scanID, AuthUsername(r)) } func (h *ReconHandler) runScan(req recon.ScanRequest, scanID, operator string) { emit := func(eventType string, payload map[string]interface{}) { if h != nil && h.wsHub != nil { h.wsHub.broadcastDashboard(Message{Type: eventType, Payload: mustMarshal(payload)}) } } report, err := recon.ScanStream(req, scanID, emit) if err != nil { if h != nil && h.wsHub != nil { h.wsHub.broadcastDashboard(Message{ Type: "recon_complete", Payload: mustMarshal(map[string]interface{}{"scan_id": scanID, "host": req.Host, "status": "failed", "error": err.Error()}), }) } return } report.ScanID = scanID if report.Status == "" { report.Status = "complete" } if req.SSRFCanary { h.finalizeSSRfCanary(scanID, report) } if h != nil && h.db != nil { _ = h.db.InsertReconScan(report) } if h != nil && h.wsHub != nil { h.wsHub.broadcastDashboard(Message{ Type: "recon_complete", Payload: mustMarshal(map[string]interface{}{ "scan_id": scanID, "host": report.Host, "status": report.Status, "profile": report.Profile, }), }) } emitter := &HubSeerEmitter{Hub: oathHub(h), DB: h.db} _ = emitter.EmitSeerEvent("recon_complete", "", map[string]interface{}{ "scan_id": scanID, "host": report.Host, "profile": report.Profile, "open_ports": openPortList(report.Ports), }) if h != nil && h.db != nil { _ = (&OathLedgerBridge{DB: h.db, Hub: oathHub(h)}).Record( operator, dbpkg.OathReconScan, "", "", dbpkg.OathOutcomeSuccess, map[string]string{"host": report.Host, "scan_id": scanID}, map[string]interface{}{ "host": report.Host, "scan_id": scanID, "open_ports": openPortList(report.Ports), "ssrf_score": crawlSSRFScore(report.Crawl), "recommendations": len(report.Recommendations), "cms_fingerprints": crawlCMS(report.Crawl), }, ) } } // GET /api/v1/recon/history?host= func (h *ReconHandler) History(w http.ResponseWriter, r *http.Request) { host := strings.TrimSpace(r.URL.Query().Get("host")) if host == "" { http.Error(w, "host required", http.StatusBadRequest) return } nhost, err := recon.NormalizeHost(host) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if h == nil || h.db == nil { writeJSON(w, map[string]interface{}{"host": nhost, "history": []recon.ReconHistoryEntry{}}) return } rows, err := h.db.ListReconScansByHost(nhost, 10) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } writeJSON(w, map[string]interface{}{"host": nhost, "history": recon.BuildHistory(rows)}) } // GET /api/v1/recon/export/{scan_id}?format=json|pdf func (h *ReconHandler) Export(w http.ResponseWriter, r *http.Request) { scanID := strings.TrimSpace(chi.URLParam(r, "scan_id")) if scanID == "" { http.Error(w, "scan_id required", http.StatusBadRequest) return } format := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("format"))) if format == "" { format = "json" } if h == nil || h.db == nil { http.Error(w, "database unavailable", http.StatusServiceUnavailable) return } report, err := h.db.GetReconScan(scanID) if err != nil { http.Error(w, "scan not found", http.StatusNotFound) return } switch format { case "json": writeJSON(w, report) case "pdf": w.Header().Set("Content-Type", "application/pdf") w.Header().Set("Content-Disposition", `attachment; filename="recon-`+scanID+`.pdf"`) _, _ = w.Write(recon.ReportToPDF(report)) default: http.Error(w, "format must be json or pdf", http.StatusBadRequest) } } func (h *ReconHandler) registerSSRfCanary(scanID, host string, r *http.Request) { base := "" if h.publicURL != nil { base = strings.TrimSpace(h.publicURL()) } if base == "" && r != nil { hh := strings.TrimSpace(r.Header.Get("X-Forwarded-Host")) if hh == "" { hh = r.Host } proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")) if proto == "" { proto = "https" } if hh != "" { base = proto + "://" + hh } } url := recon.BuildSSRfCanaryURL(base, scanID) info := &recon.SSRFCanaryInfo{ScanID: scanID, URL: url, Status: "pending", PasteTarget: url} if h.db != nil { _ = h.db.InsertReconSSRfCanary(scanID, host, url, "", "") } if h.canaryHub != nil { h.canaryHub.Register(scanID, info) } } func (h *ReconHandler) finalizeSSRfCanary(scanID string, report *recon.ScanReport) { info, _ := h.getCanaryInfo(scanID) if info == nil || report == nil || report.Crawl == nil { return } recon.ApplyCanaryPasteTarget(report.Crawl.FingerprintFields, info.URL) if top := recon.TopFormFieldFingerprint(report.Crawl.FingerprintFields); top != nil { info.PasteFieldName, info.PasteFieldID = top.Name, top.ID if h.canaryHub != nil { h.canaryHub.UpdatePasteField(scanID, top.Name, top.ID) } if h.db != nil { _ = h.db.UpdateReconSSRfCanaryPasteField(scanID, top.Name, top.ID) } } report.Canary = info } func (h *ReconHandler) getCanaryInfo(scanID string) (*recon.SSRFCanaryInfo, error) { if h.canaryHub != nil { if info := h.canaryHub.Get(scanID); info != nil { return info, nil } } if h.db != nil { return h.db.GetReconSSRfCanary(scanID) } return nil, nil } func (h *ReconHandler) CanaryStatus(w http.ResponseWriter, r *http.Request) { scanID := strings.TrimSpace(chi.URLParam(r, "scan_id")) info, err := h.getCanaryInfo(scanID) if scanID == "" || err != nil || info == nil { http.Error(w, "canary not found", http.StatusNotFound) return } writeJSON(w, info) } func (h *ReconHandler) CanaryPing(w http.ResponseWriter, r *http.Request) { scanID := strings.TrimSpace(chi.URLParam(r, "scan_id")) if scanID == "" { writeJSON(w, map[string]interface{}{"ok": false}) return } hit := h.canaryHub != nil && h.canaryHub.MarkHit(scanID) if !hit && h.db != nil { hit, _ = h.db.MarkReconSSRfCanaryHit(scanID) } if !hit { writeJSON(w, map[string]interface{}{"ok": false, "error": "canary not found"}) return } log.Printf("[recon] SSRF canary hit scan_id=%s", scanID) if h.wsHub != nil { BroadcastReconCanaryHit(h.wsHub, scanID) } writeJSON(w, map[string]interface{}{"ok": true, "scan_id": scanID, "status": "confirmed"}) } func oathHub(h *ReconHandler) *WSHub { if h == nil { return nil } return h.wsHub } func openPortList(ports []recon.PortResult) []int { var out []int for _, p := range ports { if p.Open { out = append(out, p.Port) } } return out } func crawlSSRFScore(c *recon.CrawlReport) int { if c == nil { return 0 } return c.SSRFScore } func crawlCMS(c *recon.CrawlReport) []string { if c == nil { return nil } return c.CMSFingerprints }