package store import ( "net" "net/http" "strings" "time" "github.com/pkg/errors" ) // getClientIP extracts the client IP address, checking X-Forwarded-For first func getClientIP(r *http.Request) string { // Check X-Forwarded-For header (comma-separated list, first is client) if xff := r.Header.Get("X-Forwarded-For"); xff != "" { // Take the first IP in the list ips := strings.Split(xff, ",") if len(ips) > 0 { return strings.TrimSpace(ips[0]) } } // Fall back to RemoteAddr (format: "IP:port" or "[IPv6]:port") // Use net.SplitHostPort to properly handle both IPv4 and IPv6 host, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { // If SplitHostPort fails, return as-is (shouldn't happen with valid RemoteAddr) return r.RemoteAddr } return host } // TrackRedirect increments the redirect counter for this IP+UA+Path combination // Returns the current attempt count, whether limit was exceeded, and the track details func (s *Store) TrackRedirect(r *http.Request, path string, maxAttempts int) (attempts int, exceeded bool, track *RedirectTrack) { if r == nil { return 0, false, nil } ip := getClientIP(r) userAgent := r.UserAgent() key := redirectKey(ip, userAgent, path) now := time.Now() expiresAt := now.Add(5 * time.Minute) // Try to load existing track val, exists := s.redirectTracks.Load(key) if exists { track = val.(*RedirectTrack) // Check if expired if now.After(track.ExpiresAt) { // Expired, start fresh track = &RedirectTrack{ IP: ip, UserAgent: userAgent, Path: path, Attempts: 1, FirstSeen: now, ExpiresAt: expiresAt, } s.redirectTracks.Store(key, track) return 1, false, track } // Increment existing track.Attempts++ track.ExpiresAt = expiresAt // Extend expiry exceeded = track.Attempts >= maxAttempts return track.Attempts, exceeded, track } // Create new track track = &RedirectTrack{ IP: ip, UserAgent: userAgent, Path: path, Attempts: 1, FirstSeen: now, ExpiresAt: expiresAt, } s.redirectTracks.Store(key, track) return 1, false, track } // ClearRedirectTrack removes a redirect tracking entry (called after successful completion) func (s *Store) ClearRedirectTrack(r *http.Request, path string) { if r == nil { return } ip := getClientIP(r) userAgent := r.UserAgent() key := redirectKey(ip, userAgent, path) s.redirectTracks.Delete(key) } func (t *RedirectTrack) Error(attempts int) error { return errors.Errorf( "callback redirect loop detected after %d attempts | ip=%s ua=%s path=%s first_seen=%s", attempts, t.IP, t.UserAgent, t.Path, t.FirstSeen.Format("2006-01-02T15:04:05Z07:00"), ) }