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 passwordLimiter = ratelimit.New(10, time.Minute)
23)
24
25// limitKey is "u<id>" for logged-in users, else the client IP.
26func (s *Server) limitKey(r *http.Request) string {
27 if u := User(r); u != nil {
28 return "u" + strconv.FormatInt(u.ID, 10)
29 }
30 return ratelimit.ClientIP(r, s.Cfg.TrustedProxy)
31}
32
33// allowed reports whether the request may proceed under l. byIP keys on the
34// client address instead of the session, for endpoints reachable logged out.
35// Handlers that answer in JSON call this and write their own 429.
36func (s *Server) allowed(r *http.Request, l *ratelimit.Limiter, byIP bool) bool {
37 if s.Cfg.RateLimitDisabled {
38 return true
39 }
40 key := s.limitKey(r)
41 if byIP {
42 key = ratelimit.ClientIP(r, s.Cfg.TrustedProxy)
43 }
44 return l.Allow(key)
45}
46
47// limited writes a plain-text 429 when the request exceeded l. Callers return
48// immediately when it reports true.
49func (s *Server) limited(w http.ResponseWriter, r *http.Request, l *ratelimit.Limiter, byIP bool) bool {
50 if s.allowed(r, l, byIP) {
51 return false
52 }
53 w.Header().Set("Content-Type", "text/plain; charset=utf-8")
54 w.WriteHeader(http.StatusTooManyRequests)
55 w.Write([]byte("Too many requests. Please slow down."))
56 return true
57}
58