package web import ( "encoding/json" "errors" "fmt" "net/http" "strconv" "time" ) type apiError struct { Error string `json:"error"` } func writeJSON(w http.ResponseWriter, code int, v interface{}) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(code) _ = json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, code int, msg string) { writeJSON(w, code, apiError{Error: msg}) } func methodAllowed(w http.ResponseWriter, r *http.Request, methods ...string) bool { for _, m := range methods { if r.Method == m { return true } } writeError(w, http.StatusMethodNotAllowed, "method not allowed") return false } func decodeBody(w http.ResponseWriter, r *http.Request, v interface{}) bool { r.Body = http.MaxBytesReader(w, r.Body, 4<<20) // 4 MiB is plenty for a rule file if err := json.NewDecoder(r.Body).Decode(v); err != nil { writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error()) return false } return true } // POST /api/v1/login func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodPost) { return } var req struct { Password string `json:"password"` } if !decodeBody(w, r, &req) { return } token, expiry, err := s.auth.login(r.RemoteAddr, req.Password) if err != nil { code := http.StatusUnauthorized if errors.Is(err, errTooManyAttempts) { code = http.StatusTooManyRequests } writeError(w, code, err.Error()) return } http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: token, Path: "/", Expires: expiry, HttpOnly: true, Secure: s.TLS(), SameSite: http.SameSiteLaxMode, }) writeJSON(w, http.StatusOK, map[string]interface{}{ "token": token, "expiresAt": expiry.UnixMilli(), }) } // POST /api/v1/logout func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodPost) { return } s.auth.logout(bearerToken(r)) http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, Secure: s.TLS(), SameSite: http.SameSiteLaxMode, }) writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } // GET /api/v1/info func (s *Server) handleInfo(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodGet) { return } var info Info if s.config.Info != nil { info = s.config.Info() } writeJSON(w, http.StatusOK, info) } // GET /api/v1/meta func (s *Server) handleMeta(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodGet) { return } writeJSON(w, http.StatusOK, s.config.Meta) } // GET /api/v1/metrics func (s *Server) handleMetrics(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodGet) { return } writeJSON(w, http.StatusOK, s.config.Hub.Metrics()) } // GET /api/v1/events?limit=200 func (s *Server) handleEvents(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodGet) { return } limit := 200 if v := r.URL.Query().Get("limit"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { limit = n } } writeJSON(w, http.StatusOK, map[string]interface{}{ "events": s.config.Hub.Events(limit), }) } // GET /api/v1/live - server-sent events carrying live events and metrics. func (s *Server) handleLive(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodGet) { return } flusher, ok := w.(http.Flusher) if !ok { writeError(w, http.StatusInternalServerError, "streaming unsupported") return } h := w.Header() h.Set("Content-Type", "text/event-stream") h.Set("Cache-Control", "no-cache") h.Set("Connection", "keep-alive") h.Set("X-Accel-Buffering", "no") w.WriteHeader(http.StatusOK) flusher.Flush() events, unsubscribe := s.config.Hub.Subscribe() defer unsubscribe() metricsTicker := time.NewTicker(2 * time.Second) defer metricsTicker.Stop() keepAlive := time.NewTicker(20 * time.Second) defer keepAlive.Stop() send := func(event string, v interface{}) bool { data, err := json.Marshal(v) if err != nil { return true } if _, err := fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, data); err != nil { return false } flusher.Flush() return true } if !send("metrics", s.config.Hub.Metrics()) { return } for { select { case <-r.Context().Done(): return case ev, ok := <-events: if !ok { return } if !send("event", ev) { return } case <-metricsTicker.C: if !send("metrics", s.config.Hub.Metrics()) { return } case <-keepAlive.C: if _, err := fmt.Fprint(w, ": ping\n\n"); err != nil { return } flusher.Flush() } } } type rulesRequest struct { Raw string `json:"raw"` Rules []Rule `json:"rules"` } type rulesResponse struct { Path string `json:"path"` Raw string `json:"raw"` Rules []Rule `json:"rules"` } // resolveRaw turns a request into rule file content. func (s *Server) resolveRaw(req rulesRequest) (string, error) { if req.Rules != nil { return s.config.Rules.Marshal(req.Rules) } return req.Raw, nil } // GET/PUT /api/v1/rules func (s *Server) handleRules(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodGet, http.MethodPut) { return } if s.config.Rules == nil { writeError(w, http.StatusNotImplemented, "rule management is unavailable") return } if r.Method == http.MethodGet { raw, rules, err := s.config.Rules.Load() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, rulesResponse{Path: s.config.Rules.Path(), Raw: raw, Rules: rules}) return } var req rulesRequest if !decodeBody(w, r, &req) { return } raw, err := s.resolveRaw(req) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } rules, err := s.config.Rules.Apply(raw) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } s.config.Logf("ruleset updated from web UI (%d rules)", len(rules)) writeJSON(w, http.StatusOK, rulesResponse{Path: s.config.Rules.Path(), Raw: raw, Rules: rules}) } // POST /api/v1/rules/validate func (s *Server) handleRulesValidate(w http.ResponseWriter, r *http.Request) { if !methodAllowed(w, r, http.MethodPost) { return } if s.config.Rules == nil { writeError(w, http.StatusNotImplemented, "rule management is unavailable") return } var req rulesRequest if !decodeBody(w, r, &req) { return } raw, err := s.resolveRaw(req) if err != nil { writeJSON(w, http.StatusOK, map[string]interface{}{"valid": false, "error": err.Error()}) return } rules, err := s.config.Rules.Validate(raw) if err != nil { writeJSON(w, http.StatusOK, map[string]interface{}{ "valid": false, "error": err.Error(), "raw": raw, }) return } writeJSON(w, http.StatusOK, map[string]interface{}{ "valid": true, "rules": rules, "raw": raw, }) }