package web import ( "net/http" "strconv" "time" "hearthforge/internal/ratelimit" ) // Rate limits. The window is one minute unless noted. var ( loginLimiter = ratelimit.New(10, time.Minute) registrationLimiter = ratelimit.New(3, time.Hour) gitAuthLimiter = ratelimit.New(10, time.Minute) commentLimiter = ratelimit.New(30, time.Minute) reactionLimiter = ratelimit.New(60, time.Minute) issueCreateLimiter = ratelimit.New(10, time.Minute) patchCreateLimiter = ratelimit.New(10, time.Minute) labelWriteLimiter = ratelimit.New(30, time.Minute) uploadLimiter = ratelimit.New(10, time.Minute) passwordLimiter = ratelimit.New(10, time.Minute) ) // limitKey is "u" for logged-in users, else the client IP. func (s *Server) limitKey(r *http.Request) string { if u := User(r); u != nil { return "u" + strconv.FormatInt(u.ID, 10) } return ratelimit.ClientIP(r, s.Cfg.TrustedProxy) } // allowed reports whether the request may proceed under l. byIP keys on the // client address instead of the session, for endpoints reachable logged out. // Handlers that answer in JSON call this and write their own 429. func (s *Server) allowed(r *http.Request, l *ratelimit.Limiter, byIP bool) bool { if s.Cfg.RateLimitDisabled { return true } key := s.limitKey(r) if byIP { key = ratelimit.ClientIP(r, s.Cfg.TrustedProxy) } return l.Allow(key) } // limited writes a plain-text 429 when the request exceeded l. Callers return // immediately when it reports true. func (s *Server) limited(w http.ResponseWriter, r *http.Request, l *ratelimit.Limiter, byIP bool) bool { if s.allowed(r, l, byIP) { return false } w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusTooManyRequests) w.Write([]byte("Too many requests. Please slow down.")) return true }