package handlers import ( "crypto/rand" "encoding/base64" "net/http" "sync" "time" ) 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) }