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:
+63
-14
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user