feat: отмена скана на сервере, стабильнее scan_id, упрощённый UI

- Runner: context, POST /api/scans/{id}/cancel, частичный canceled; Start до ответа createScan

- API: scanPathID (PathUnescape), лог при 404; Postgres GetScan логирует ошибки чтения

- newScanID: суффикс 8 hex; тест обновлён

- Web UI: убраны строки scan_id/from-to; скрытое поле scanId; cancel при таймауте/новом скане

Made-with: Cursor
This commit is contained in:
PTah
2026-04-13 15:26:23 +10:00
parent 5b3eeaed45
commit 088be32ec5
6 changed files with 224 additions and 210 deletions
+63 -14
View File
@@ -2,7 +2,9 @@ package api
import (
"encoding/json"
"log"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
@@ -70,6 +72,17 @@ func NewHandler(store scans.Store, run *scans.Runner, resetDBSecret string) *Han
return &Handler{store: store, run: run, resetDBSecret: strings.TrimSpace(resetDBSecret)}
}
func scanPathID(r *http.Request) string {
raw := r.PathValue("id")
if raw == "" {
return ""
}
if dec, err := url.PathUnescape(raw); err == nil {
return strings.TrimSpace(dec)
}
return strings.TrimSpace(raw)
}
func (h *Handler) Routes() http.Handler {
mux := http.NewServeMux()
webFS := http.FileServer(http.FS(webui.FS))
@@ -80,6 +93,7 @@ func (h *Handler) Routes() http.Handler {
mux.HandleFunc("GET /api/scans", h.listScans)
mux.HandleFunc("GET /api/scans/diff", h.getScansDiff)
mux.HandleFunc("GET /api/scans/{id}", h.getScan)
mux.HandleFunc("POST /api/scans/{id}/cancel", h.postCancelScan)
mux.HandleFunc("GET /api/scans/{id}/hosts", h.getScanHosts)
mux.HandleFunc("GET /api/scans/{id}/ports", h.getScanPorts)
mux.HandleFunc("GET /api/scans/{id}/snmp", h.getScanSNMP)
@@ -142,14 +156,15 @@ func (h *Handler) createScan(w http.ResponseWriter, r *http.Request) {
return
}
// Start до ответа клиенту: иначе возможен POST /cancel до регистрации cancel в Runner (задача останется queued/running).
if h.run != nil {
h.run.Start(job)
}
writeJSON(w, http.StatusCreated, map[string]any{
"scan_id": job.ID,
"status": job.Status,
})
if h.run != nil {
h.run.Start(job)
}
}
func (h *Handler) listScans(w http.ResponseWriter, r *http.Request) {
@@ -200,7 +215,7 @@ func (h *Handler) getScansDiff(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScan(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -208,6 +223,7 @@ func (h *Handler) getScan(w http.ResponseWriter, r *http.Request) {
job, ok := h.store.GetScan(id)
if !ok {
log.Printf("getScan: not found id=%q (len=%d)", id, len(id))
writeError(w, http.StatusNotFound, "scan not found")
return
}
@@ -215,8 +231,41 @@ func (h *Handler) getScan(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, job)
}
func (h *Handler) postCancelScan(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "use POST")
return
}
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
}
if _, ok := h.store.GetScan(id); !ok {
log.Printf("postCancelScan: not found id=%q (len=%d)", id, len(id))
writeError(w, http.StatusNotFound, "scan not found")
return
}
runnerSignaled := false
if h.run != nil {
runnerSignaled = h.run.CancelScan(id)
}
if runnerSignaled {
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "runner_canceled": true})
return
}
cur, _ := h.store.GetScan(id)
if cur.Status == "queued" {
fin := time.Now().UTC()
_, _ = h.store.UpdateScan(id, scans.ScanUpdate{Status: "canceled", FinishedAt: &fin})
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "runner_canceled": false, "status": "canceled"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true, "runner_canceled": false})
}
func (h *Handler) getScanHosts(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -242,7 +291,7 @@ func (h *Handler) getScanHosts(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanPorts(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -268,7 +317,7 @@ func (h *Handler) getScanPorts(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanSNMP(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -294,7 +343,7 @@ func (h *Handler) getScanSNMP(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanLLDP(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -320,7 +369,7 @@ func (h *Handler) getScanLLDP(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanInterfaces(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -349,7 +398,7 @@ func (h *Handler) getScanInterfaces(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanPortDevices(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -409,7 +458,7 @@ func (h *Handler) getScanPortDevices(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -558,7 +607,7 @@ func (h *Handler) getScanMacLocations(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanLinks(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return
@@ -621,7 +670,7 @@ func (h *Handler) getScanLinks(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) getScanTopology(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
id := scanPathID(r)
if id == "" {
writeError(w, http.StatusBadRequest, "scan id is required")
return