package handlers import ( "crypto/rand" "encoding/base64" "net/http" "sync" "time" "warpbox.dev/backend/libs/services" ) const csrfCookieName = "warpbox_csrf" type rateLimiter struct { mu sync.Mutex records map[string]rateRecord } type rateRecord struct { StartedAt time.Time Count int } func newRateLimiter() *rateLimiter { return &rateLimiter{records: make(map[string]rateRecord)} } func (l *rateLimiter) Allow(key string, limit int, window time.Duration, now time.Time) bool { if limit <= 0 || window <= 0 { return true } l.mu.Lock() defer l.mu.Unlock() record := l.records[key] if record.StartedAt.IsZero() || now.Sub(record.StartedAt) >= window { l.records[key] = rateRecord{StartedAt: now, Count: 1} return true } record.Count++ l.records[key] = record return record.Count <= limit } func (a *App) csrfToken(w http.ResponseWriter, r *http.Request) string { if cookie, err := r.Cookie(csrfCookieName); err == nil && cookie.Value != "" { return cookie.Value } token := randomToken(32) http.SetCookie(w, &http.Cookie{ Name: csrfCookieName, Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: r.TLS != nil, Expires: time.Now().Add(12 * time.Hour), }) return token } func (a *App) validateCSRF(w http.ResponseWriter, r *http.Request) bool { if r.Method == http.MethodGet || r.Method == http.MethodHead || r.Method == http.MethodOptions { return true } cookie, err := r.Cookie(csrfCookieName) if err != nil || cookie.Value == "" || r.FormValue("csrf_token") != cookie.Value { http.Error(w, "invalid form token", http.StatusForbidden) return false } return true } func randomToken(byteCount int) string { data := make([]byte, byteCount) if _, err := rand.Read(data); err != nil { return base64.RawURLEncoding.EncodeToString([]byte(time.Now().String())) } return base64.RawURLEncoding.EncodeToString(data) } func (a *App) recordLoginAbuse(r *http.Request, kind, detail string) { if a.banService == nil { return } settings, err := a.banService.Settings() if err != nil || !settings.AutoBanEnabled { return } threshold := settings.UserLoginFailureThreshold if kind == services.AbuseKindAdminLogin { threshold = settings.AdminLoginFailureThreshold } ip := uploadClientIP(r) result, err := a.banService.RecordAbuse(ip, kind, detail, threshold, time.Now().UTC()) if err != nil { a.logger.Error("login abuse event failed", "source", "ban", "severity", "error", "code", 5004, "ip", ip, "kind", kind, "error", err.Error()) return } if result.Enabled { a.logger.Warn("login abuse recorded", "source", "ban", "severity", "warn", "code", 4304, "ip", ip, "kind", kind, "count", result.Event.Count) } if result.Triggered { a.logger.Warn("ip auto-banned for login abuse", "source", "ban", "severity", "warn", "code", 4305, "ip", ip, "kind", kind, "ban_id", result.Ban.ID) } }