198 lines
4.5 KiB
Go
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)
|
|
}
|