package handlers import ( "context" "fmt" "net" "net/http" "strings" "time" "crowdsec-dashy/internal/crowdsec" ) // AllowlistHandler manages the allowlist page. type AllowlistHandler struct { deps Deps } func NewAllowlistHandler(deps Deps) *AllowlistHandler { return &AllowlistHandler{deps: deps} } type AllowlistData struct { PageData Lists []crowdsec.Allowlist FetchErr string } func (h *AllowlistHandler) List(w http.ResponseWriter, r *http.Request) { pd := NewPageData(r, "Allowlist", h.deps.CLIAvailable, h.deps.PollInterval) if f := readFlash(r); f.Message != "" { pd.Flash = f } if !h.deps.CLIAvailable { h.deps.Renderer.Render(w, "allowlist", AllowlistData{ PageData: pd, FetchErr: "cscli is not available — allowlist management requires the cscli binary.", }) return } ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second) defer cancel() lists, err := h.deps.CLI.ListAllowlists(ctx) fetchErr := "" if err != nil { fetchErr = err.Error() } h.deps.Renderer.Render(w, "allowlist", AllowlistData{ PageData: pd, Lists: lists, FetchErr: fetchErr, }) } // CreateList creates a new named allowlist. func (h *AllowlistHandler) CreateList(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 1024) if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { flashRedirect(w, r, "/allowlist", "error", "list name is required") return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() if err := h.deps.CLI.CreateAllowlist(ctx, name); err != nil { flashRedirect(w, r, "/allowlist", "error", "create failed: "+err.Error()) return } flashRedirect(w, r, "/allowlist", "success", "Allowlist "+name+" created") } // AddEntry adds one or more IPs/CIDRs to an allowlist. Auto-creates the list if missing. func (h *AllowlistHandler) AddEntry(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 16384) if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } listName := strings.TrimSpace(r.FormValue("list")) raw := r.FormValue("value") if listName == "" { flashRedirect(w, r, "/allowlist", "error", "list name is required") return } comment := strings.TrimSpace(r.FormValue("comment")) values, err := parseAllowlistValues(raw) if err != nil { flashRedirect(w, r, "/allowlist", "error", err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() // Auto-create the list — CreateAllowlist is a no-op if it already exists. if err := h.deps.CLI.CreateAllowlist(ctx, listName); err != nil { flashRedirect(w, r, "/allowlist", "error", "create list failed: "+err.Error()) return } if err := h.deps.CLI.AddAllowlistEntries(ctx, listName, comment, values); err != nil { flashRedirect(w, r, "/allowlist", "error", "add failed: "+err.Error()) return } msg := fmt.Sprintf("%d entr%s added to %s", len(values), pluralY(len(values)), listName) flashRedirect(w, r, "/allowlist", "success", msg) } // DeleteList removes an entire allowlist. func (h *AllowlistHandler) DeleteList(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 1024) if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { flashRedirect(w, r, "/allowlist", "error", "list name is required") return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() if err := h.deps.CLI.DeleteAllowlist(ctx, name); err != nil { flashRedirect(w, r, "/allowlist", "error", "delete failed: "+err.Error()) return } flashRedirect(w, r, "/allowlist", "success", "Allowlist "+name+" deleted") } func (h *AllowlistHandler) RemoveEntry(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 4096) if err := r.ParseForm(); err != nil { http.Error(w, "bad request", http.StatusBadRequest) return } if !checkCSRF(r) { http.Error(w, "forbidden", http.StatusForbidden) return } listName := strings.TrimSpace(r.FormValue("list")) value := strings.TrimSpace(r.FormValue("value")) if listName == "" || value == "" { flashRedirect(w, r, "/allowlist", "error", "list name and value are required") return } ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second) defer cancel() if err := h.deps.CLI.RemoveAllowlistEntry(ctx, listName, value); err != nil { flashRedirect(w, r, "/allowlist", "error", "remove failed: "+err.Error()) return } flashRedirect(w, r, "/allowlist", "success", value+" removed from "+listName) } // parseAllowlistValues splits a raw multi-value string (newline/comma/space delimited) // and validates each entry as an IP address or CIDR range. func parseAllowlistValues(raw string) ([]string, error) { fields := strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' || r == ' ' || r == '\t' }) var out []string for _, f := range fields { f = strings.TrimSpace(f) if f == "" { continue } if net.ParseIP(f) == nil { if _, _, err := net.ParseCIDR(f); err != nil { return nil, fmt.Errorf("invalid IP or CIDR: %q", f) } } out = append(out, f) } if len(out) == 0 { return nil, fmt.Errorf("no valid IP addresses or CIDR ranges provided") } return out, nil } func pluralY(n int) string { if n == 1 { return "y" } return "ies" }