limits.go
⎇
Raw
1package web
2
3import (
4 "net/http"
5 "strconv"
6 "time"
7
8 "hearthforge/internal/ratelimit"
9)
10
11// Rate limits. The window is one minute unless noted.
12var (
13 loginLimiter = ratelimit.New(10, time.Minute)
14 registrationLimiter = ratelimit.New(3, time.Hour)
15 gitAuthLimiter = ratelimit.New(10, time.Minute)
16 commentLimiter = ratelimit.New(30, time.Minute)
17 reactionLimiter = ratelimit.New(60, time.Minute)
18 issueCreateLimiter = ratelimit.New(10, time.Minute)
19 patchCreateLimiter = ratelimit.New(10, time.Minute)
20 labelWriteLimiter = ratelimit.New(30, time.Minute)
21 uploadLimiter = ratelimit.New(10, time.Minute)
22)
23
24// limitKey is "u<id>" for logged-in users, else the client IP.
25func (s *Server) limitKey(r *http.Request) string {
26 if u := User(r); u != nil {
27 return "u" + strconv.FormatInt(u.ID, 10)
28 }
29 return ratelimit.ClientIP(r, s.Cfg.TrustedProxy)
30}
31
32// allowed reports whether the request may proceed under l. byIP keys on the
33// client address instead of the session, for endpoints reachable logged out.
34// Handlers that answer in JSON call this and write their own 429.
35func (s *Server) allowed(r *http.Request, l *ratelimit.Limiter, byIP bool) bool {
36 if s.Cfg.RateLimitDisabled {
37 return true
38 }
39 key := s.limitKey(r)
40 if byIP {
41 key = ratelimit.ClientIP(r, s.Cfg.TrustedProxy)
42 }
43 return key == "" || l.Allow(key)
44}
45
46// limited writes a plain-text 429 when the request exceeded l. Callers return
47// immediately when it reports true.
48func (s *Server) limited(w http.ResponseWriter, r *http.Request, l *ratelimit.Limiter, byIP bool) bool {
49 if s.allowed(r, l, byIP) {
50 return false
51 }
52 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
53 w.WriteHeader(http.StatusTooManyRequests)
54 w.Write([]byte("Too many requests. Please slow down."))
55 return true
56}
57