Files
crowdsec-dashy/internal/handlers/countries.go
T

198 lines
4.5 KiB
Go

package handlers
import (
"context"
"net/http"
"strconv"
"strings"
"time"
"crowdsec-dashy/internal/crowdsec"
)
// CountriesHandler manages country-scope decisions.
type CountriesHandler struct {
deps Deps
}
func NewCountriesHandler(deps Deps) *CountriesHandler {
return &CountriesHandler{deps: deps}
}
type CountriesData struct {
PageData
Decisions []crowdsec.Decision
Page int
HasNext bool
PageSize int
}
const countriesPageSize = 50
func (h *CountriesHandler) List(w http.ResponseWriter, r *http.Request) {
if !h.deps.CLIAvailable {
h.deps.Renderer.Render(w, "countries", CountriesData{
PageData: NewPageData(r, "Countries", false, h.deps.PollInterval),
PageSize: countriesPageSize,
})
return
}
page := 1
if p, err := strconv.Atoi(r.URL.Query().Get("page")); err == nil && p > 1 {
page = p
}
offset := (page - 1) * countriesPageSize
ctx, cancel := context.WithTimeout(r.Context(), 20*time.Second)
defer cancel()
decisions, err := h.deps.CLI.ListDecisions(ctx, crowdsec.DecisionFilter{
Scope: "Country",
Limit: countriesPageSize + 1,
Offset: offset,
})
pd := NewPageData(r, "Countries", h.deps.CLIAvailable, h.deps.PollInterval)
if f := readFlash(r); f.Message != "" {
pd.Flash = f
}
if err != nil {
pd = pd.WithFlash("error", "Failed to load country decisions: "+err.Error())
}
hasNext := false
if len(decisions) > countriesPageSize {
hasNext = true
decisions = decisions[:countriesPageSize]
}
h.deps.Renderer.Render(w, "countries", CountriesData{
PageData: pd,
Decisions: decisions,
Page: page,
HasNext: hasNext,
PageSize: countriesPageSize,
})
}
func (h *CountriesHandler) Add(w http.ResponseWriter, r *http.Request) {
if !h.deps.CLIAvailable {
http.Redirect(w, r, "/countries", http.StatusSeeOther)
return
}
r.Body = http.MaxBytesReader(w, r.Body, 8192)
if err := r.ParseForm(); err != nil {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
if !checkCSRF(r) {
http.Error(w, "forbidden", http.StatusForbidden)
return
}
// Parse comma/newline/space separated country codes.
raw := strings.ToUpper(r.FormValue("countries"))
var codes []string
for _, part := range strings.FieldsFunc(raw, func(r rune) bool {
return r == ',' || r == '\n' || r == '\r' || r == ' '
}) {
code := strings.TrimSpace(part)
if code == "" {
continue
}
if len(code) != 2 {
flashRedirect(w, r, "/countries", "error", "invalid country code: "+code)
return
}
for _, c := range code {
if c < 'A' || c > 'Z' {
flashRedirect(w, r, "/countries", "error", "invalid country code: "+code)
return
}
}
codes = append(codes, code)
}
if len(codes) == 0 {
flashRedirect(w, r, "/countries", "error", "at least one country code required")
return
}
decType := r.FormValue("type")
switch decType {
case "ban", "captcha", "throttle":
default:
decType = "ban"
}
duration := strings.TrimSpace(r.FormValue("duration"))
if r.FormValue("permanent") == "1" {
duration = "87600h" // 10 years — cscli has no true permanent
}
if duration == "" {
duration = "24h"
}
if !durationRE.MatchString(duration) {
flashRedirect(w, r, "/countries", "error", "invalid duration: "+duration)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
var errs []string
for _, code := range codes {
err := h.deps.CLI.AddDecision(ctx, crowdsec.DecisionInput{
Scope: "Country",
Value: code,
Type: decType,
Duration: duration,
Origin: "cscli",
})
if err != nil {
errs = append(errs, code+": "+err.Error())
}
}
if len(errs) > 0 {
flashRedirect(w, r, "/countries", "error", strings.Join(errs, "; "))
return
}
flashRedirect(w, r, "/countries", "success",
strings.Join(codes, ", ")+" added as "+decType+" ("+duration+")")
}
func (h *CountriesHandler) Delete(w http.ResponseWriter, r *http.Request) {
if !h.deps.CLIAvailable {
http.Redirect(w, r, "/countries", http.StatusSeeOther)
return
}
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
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
for _, idStr := range r.Form["id"] {
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
continue
}
_ = h.deps.CLI.DeleteDecision(ctx, id)
}
http.Redirect(w, r, "/countries", http.StatusSeeOther)
}