Files
jbergner 7c69432097
Some checks failed
release-tag / release-image (push) Successful in 2m3s
release-main / release-images (push) Failing after 3m9s
RC-2 0.5.2
2026-08-24 05:46:42 +02:00

81 lines
2.3 KiB
Go

package edgeguard
import (
"encoding/json"
"log/slog"
"net/http"
"net/netip"
"strconv"
"strings"
"time"
)
type HTTPServer struct {
guard *Guard
logger *slog.Logger
}
func NewHTTPServer(g *Guard, logger *slog.Logger) *HTTPServer {
if logger == nil {
logger = slog.Default()
}
return &HTTPServer{guard: g, logger: logger}
}
func (s *HTTPServer) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", s.healthz)
mux.HandleFunc("GET /metrics", s.metrics)
mux.HandleFunc("GET /check", s.check)
return mux
}
func (s *HTTPServer) healthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusNoContent)
}
func (s *HTTPServer) metrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
w.Header().Set("Cache-Control", "no-store")
_, _ = w.Write([]byte(s.guard.Metrics()))
}
func (s *HTTPServer) check(w http.ResponseWriter, r *http.Request) {
rawIP := strings.TrimSpace(r.Header.Get("X-Edge-Client-IP"))
ip, err := netip.ParseAddr(rawIP)
if err != nil {
s.writeDeny(w, http.StatusBadRequest, "invalid client ip", 0)
return
}
host := r.Header.Get("X-Edge-Original-Host")
method := r.Header.Get("X-Edge-Original-Method")
uri := r.Header.Get("X-Edge-Original-URI")
decision := s.guard.Check(ip.Unmap(), host, method, uri, time.Now())
if decision.Allowed {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusNoContent)
return
}
// Do not log every denial here: during an attack that would turn logging
// itself into a resource-exhaustion vector. Caddy access logs are sampled
// and EdgeGuard exposes counters; only state changes such as auto-bans are
// emitted as runtime log events.
s.writeDeny(w, decision.StatusCode, decision.Reason, decision.RetryAfter)
}
func (s *HTTPServer) writeDeny(w http.ResponseWriter, code int, reason string, retryAfter int) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
if retryAfter > 0 {
w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
}
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": http.StatusText(code),
"reason": reason,
})
}