- Add Cleanup() method (caddy.CleanerUpper) to stop goroutines on config reload, preventing goroutine leaks - Add Validate() method (caddy.Validator) for early config validation with reasonable bounds checking - Add public BanIP() method for admin handler, replacing direct internal state manipulation - Add bounds checking for failure tracker and ban maps to prevent memory exhaustion under DDoS (100k/50k limits) - Add eviction functions to proactively clean oldest entries when at capacity
248 lines
6.8 KiB
Go
248 lines
6.8 KiB
Go
package sipguardian
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/caddyserver/caddy/v2"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
func init() {
|
|
caddy.RegisterModule(AdminHandler{})
|
|
httpcaddyfile.RegisterHandlerDirective("sip_guardian_admin", parseSIPGuardianAdmin)
|
|
// Register handler ordering so it can be used directly in handle blocks
|
|
httpcaddyfile.RegisterDirectiveOrder("sip_guardian_admin", httpcaddyfile.Before, "respond")
|
|
}
|
|
|
|
// parseSIPGuardianAdmin parses the sip_guardian_admin directive
|
|
func parseSIPGuardianAdmin(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
|
var handler AdminHandler
|
|
err := handler.UnmarshalCaddyfile(h.Dispenser)
|
|
return &handler, err
|
|
}
|
|
|
|
// AdminHandler provides HTTP endpoints to manage SIP Guardian
|
|
type AdminHandler struct {
|
|
guardian *SIPGuardian
|
|
}
|
|
|
|
func (AdminHandler) CaddyModule() caddy.ModuleInfo {
|
|
return caddy.ModuleInfo{
|
|
ID: "http.handlers.sip_guardian_admin",
|
|
New: func() caddy.Module { return new(AdminHandler) },
|
|
}
|
|
}
|
|
|
|
func (h *AdminHandler) Provision(ctx caddy.Context) error {
|
|
// Get the shared guardian instance from the global registry
|
|
guardian, err := GetOrCreateGuardian(ctx, "default")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
h.guardian = guardian
|
|
return nil
|
|
}
|
|
|
|
// ServeHTTP handles admin API requests
|
|
func (h *AdminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
|
// Route based on path
|
|
path := r.URL.Path
|
|
|
|
switch {
|
|
case strings.HasSuffix(path, "/bans"):
|
|
return h.handleBans(w, r)
|
|
case strings.HasSuffix(path, "/stats"):
|
|
return h.handleStats(w, r)
|
|
case strings.HasSuffix(path, "/dns-whitelist"):
|
|
return h.handleDNSWhitelist(w, r)
|
|
case strings.HasSuffix(path, "/dns-whitelist/refresh"):
|
|
return h.handleDNSWhitelistRefresh(w, r)
|
|
case strings.Contains(path, "/unban/"):
|
|
return h.handleUnban(w, r, path)
|
|
case strings.Contains(path, "/ban/"):
|
|
return h.handleBan(w, r, path)
|
|
default:
|
|
return next.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// handleBans lists all banned IPs
|
|
func (h *AdminHandler) handleBans(w http.ResponseWriter, r *http.Request) error {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return nil
|
|
}
|
|
|
|
bans := h.guardian.GetBannedIPs()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
return json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"bans": bans,
|
|
"count": len(bans),
|
|
})
|
|
}
|
|
|
|
// handleStats returns current statistics
|
|
func (h *AdminHandler) handleStats(w http.ResponseWriter, r *http.Request) error {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return nil
|
|
}
|
|
|
|
stats := h.guardian.GetStats()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
return json.NewEncoder(w).Encode(stats)
|
|
}
|
|
|
|
// handleUnban removes an IP from the ban list
|
|
func (h *AdminHandler) handleUnban(w http.ResponseWriter, r *http.Request, path string) error {
|
|
if r.Method != http.MethodPost && r.Method != http.MethodDelete {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return nil
|
|
}
|
|
|
|
// Extract IP from path: /admin/sip-guardian/unban/{ip}
|
|
parts := strings.Split(path, "/unban/")
|
|
if len(parts) != 2 || parts[1] == "" {
|
|
http.Error(w, "IP address required", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
ip := strings.TrimSuffix(parts[1], "/")
|
|
|
|
if h.guardian.UnbanIP(ip) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "IP unbanned",
|
|
"ip": ip,
|
|
})
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": false,
|
|
"message": "IP not found in ban list",
|
|
"ip": ip,
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleDNSWhitelist returns DNS whitelist entries and stats
|
|
func (h *AdminHandler) handleDNSWhitelist(w http.ResponseWriter, r *http.Request) error {
|
|
if r.Method != http.MethodGet {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return nil
|
|
}
|
|
|
|
entries := h.guardian.GetDNSWhitelistEntries()
|
|
stats := h.guardian.GetStats()
|
|
|
|
response := map[string]interface{}{
|
|
"entries": entries,
|
|
"count": len(entries),
|
|
}
|
|
|
|
// Add DNS-specific stats if available
|
|
if dnsStats, ok := stats["dns_whitelist"]; ok {
|
|
response["stats"] = dnsStats
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
return json.NewEncoder(w).Encode(response)
|
|
}
|
|
|
|
// handleDNSWhitelistRefresh forces an immediate DNS refresh
|
|
func (h *AdminHandler) handleDNSWhitelistRefresh(w http.ResponseWriter, r *http.Request) error {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return nil
|
|
}
|
|
|
|
h.guardian.RefreshDNSWhitelist()
|
|
|
|
// Get updated entries
|
|
entries := h.guardian.GetDNSWhitelistEntries()
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
return json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "DNS whitelist refreshed",
|
|
"count": len(entries),
|
|
})
|
|
}
|
|
|
|
// handleBan manually adds an IP to the ban list
|
|
func (h *AdminHandler) handleBan(w http.ResponseWriter, r *http.Request, path string) error {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return nil
|
|
}
|
|
|
|
// Extract IP from path: /admin/sip-guardian/ban/{ip}
|
|
parts := strings.Split(path, "/ban/")
|
|
if len(parts) != 2 || parts[1] == "" {
|
|
http.Error(w, "IP address required", http.StatusBadRequest)
|
|
return nil
|
|
}
|
|
|
|
ip := strings.TrimSuffix(parts[1], "/")
|
|
|
|
// Parse optional reason from body
|
|
var body struct {
|
|
Reason string `json:"reason"`
|
|
}
|
|
if r.Body != nil {
|
|
json.NewDecoder(r.Body).Decode(&body)
|
|
}
|
|
if body.Reason == "" {
|
|
body.Reason = "manual_ban"
|
|
}
|
|
|
|
// Use public BanIP method for proper encapsulation
|
|
h.guardian.BanIP(ip, body.Reason)
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
return json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"success": true,
|
|
"message": "IP banned",
|
|
"ip": ip,
|
|
"reason": body.Reason,
|
|
})
|
|
}
|
|
|
|
// UnmarshalCaddyfile implements caddyfile.Unmarshaler for AdminHandler.
|
|
// Usage in Caddyfile:
|
|
//
|
|
// handle /api/sip-guardian/* {
|
|
// sip_guardian_admin
|
|
// }
|
|
//
|
|
// Or simply: sip_guardian_admin
|
|
func (h *AdminHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|
// Move past "sip_guardian_admin" token
|
|
d.Next()
|
|
|
|
// This handler doesn't have any configuration options currently
|
|
// but we need to consume any block if present
|
|
for nesting := d.Nesting(); d.NextBlock(nesting); {
|
|
return d.Errf("unknown sip_guardian_admin directive: %s", d.Val())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Interface guards
|
|
var (
|
|
_ caddyhttp.MiddlewareHandler = (*AdminHandler)(nil)
|
|
_ caddy.Provisioner = (*AdminHandler)(nil)
|
|
_ caddyfile.Unmarshaler = (*AdminHandler)(nil)
|
|
)
|