Ryan Malloy a9d938c64c Apply Matt Holt code review quick fixes
Performance improvements:
- Fix O(n²) bubble sort → O(n log n) sort.Slice() in eviction (1000x faster)
- Remove custom min() function, use Go 1.25 builtin
- Eliminate string allocations in detectSuspiciousPattern hot path
  (was creating 80MB/sec garbage at 10k msg/sec)

Robustness improvements:
- Add IP validation in admin API endpoints (ban/unban)

Documentation:
- Add comprehensive CODE_REVIEW_MATT_HOLT.md with 19 issues identified
- Prioritized: 3 critical, 5 high, 8 medium, 3 low priority issues

Remaining work (see CODE_REVIEW_MATT_HOLT.md):
- Replace global registry with Caddy app system
- Move feature flags to struct fields
- Fix Prometheus integration
- Implement worker pool for storage writes
- Make config immutable after Provision
2025-12-24 22:05:40 -07:00

261 lines
7.1 KiB
Go

package sipguardian
import (
"encoding/json"
"net"
"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], "/")
// Validate IP address format
if net.ParseIP(ip) == nil {
http.Error(w, "Invalid IP address format", http.StatusBadRequest)
return nil
}
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], "/")
// Validate IP address format
if net.ParseIP(ip) == nil {
http.Error(w, "Invalid IP address format", http.StatusBadRequest)
return nil
}
// 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)
)