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) )