package middleware import ( "net/http" "strings" "sync" "time" ) // contactRateLimitEntry tracks rate limiting for contact form per IP type contactRateLimitEntry struct { count int resetTime time.Time } // ContactRateLimiter provides rate limiting specifically for contact form // Allows 5 submissions per hour per IP address type ContactRateLimiter struct { mu sync.RWMutex clients map[string]*contactRateLimitEntry } // NewContactRateLimiter creates a new contact form rate limiter // Default: 5 requests per hour per IP func NewContactRateLimiter() *ContactRateLimiter { rl := &ContactRateLimiter{ clients: make(map[string]*contactRateLimitEntry), } // Cleanup expired entries every 10 minutes go rl.cleanup() return rl } // Middleware returns rate limiting middleware for contact form func (rl *ContactRateLimiter) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Get client IP (handle X-Forwarded-For for proxies) ip := r.Header.Get("X-Forwarded-For") if ip == "" { ip = r.Header.Get("X-Real-IP") } if ip == "" { ip = strings.Split(r.RemoteAddr, ":")[0] } // Extract first IP if multiple IPs in X-Forwarded-For if strings.Contains(ip, ",") { ip = strings.TrimSpace(strings.Split(ip, ",")[0]) } if !rl.allow(ip) { // Check if HTMX request isHTMX := r.Header.Get("HX-Request") != "" if isHTMX { // Return HTMX-friendly error w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`

Too Many Requests

You've submitted too many contact forms. Please wait an hour before trying again.

`)) } else { w.Header().Set("Retry-After", "3600") // 1 hour http.Error(w, "Too many contact form submissions. Please try again in an hour.", http.StatusTooManyRequests) } return } next.ServeHTTP(w, r) }) } // allow checks if the request is allowed based on rate limit // Limit: 5 submissions per hour func (rl *ContactRateLimiter) allow(ip string) bool { rl.mu.Lock() defer rl.mu.Unlock() now := time.Now() limit := 5 window := 1 * time.Hour entry, exists := rl.clients[ip] if !exists || now.After(entry.resetTime) { // New client or window expired rl.clients[ip] = &contactRateLimitEntry{ count: 1, resetTime: now.Add(window), } return true } if entry.count >= limit { return false } entry.count++ return true } // cleanup removes expired entries periodically func (rl *ContactRateLimiter) cleanup() { ticker := time.NewTicker(10 * time.Minute) defer ticker.Stop() for range ticker.C { rl.mu.Lock() now := time.Now() for ip, entry := range rl.clients { if now.After(entry.resetTime) { delete(rl.clients, ip) } } rl.mu.Unlock() } } // GetStats returns current rate limit statistics (for monitoring/debugging) func (rl *ContactRateLimiter) GetStats() map[string]interface{} { rl.mu.RLock() defer rl.mu.RUnlock() return map[string]interface{}{ "total_clients": len(rl.clients), "limit": 5, "window": "1 hour", } }