Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/web/ratelimit.go

Line attribution on main · 230 line(s)

edf9db6e
Keenan Nemetz
12d ago
1 package web
2
3 // Rate limits for the authentication surface.
4 //
5 // The passkey ceremonies are the only endpoints an anonymous client can make the
6 // instance work on: an assertion costs elliptic-curve verification, a begin costs
7 // a WebAuthn challenge and a row in the ceremony map, and registration writes an
8 // account. They are also the endpoints worth guessing against. Everything else on
9 // the web is a session-authenticated read or a CSRF-guarded write.
10 //
11 // The limiter is a fixed window per key: a counter and the moment it resets. That
12 // is coarse compared with a bucket, but it is one number per address per minute,
13 // it cannot drift, and the size of the map is bounded by sweeping expired entries
14 // every time the table grows. Two keys are used: the client address, and the
15 // account name a begin request claims. The address bound is what stops a spray
16 // from one host; the account bound is what stops a slow guess at one name from
17 // many hosts from getting a faster answer than the same guess from one.
18
19 import (
25343521
Keenan Nemetz
2d ago
20 "crypto/sha256"
21 "encoding/hex"
edf9db6e
Keenan Nemetz
12d ago
22 "fmt"
23 "net"
24 "net/http"
25 "strconv"
26 "strings"
27 "sync"
28 "time"
29 )
30
31 const (
32 // Ceremony begins per address and per minute. A person signing in does one;
33 // a client retrying a flaky authenticator does a handful.
34 authBeginsPerAddress = 20
35 // Ceremony begins per account name and per minute. An account with a working
36 // passkey never needs more than a few.
37 authBeginsPerAccount = 10
38 // Ceremony finishes per address and per minute. A finish is one signature
39 // verification, and a browser may retry it.
40 authFinishesPerAddress = 60
41 // Registrations per address and per minute. Each one writes an account, so the
42 // budget is small even for a public instance.
43 registrationsPerAddress = 5
44 // authLimitWindow is the window every budget above is counted in.
45 authLimitWindow = time.Minute
e5542ac6
Keenan Nemetz
12d ago
46 // defaultContributorWritesPerHour bounds an untrusted account's browser writes
47 // (issues, comments, requests, labels, state changes). A person discussing never
48 // comes near it; a script posting in a loop does. Trusted roles are not
49 // budgeted at all; see writeBudgeted.
50 defaultContributorWritesPerHour = 300
edf9db6e
Keenan Nemetz
12d ago
51 // maxLimitKeys bounds the table. Keys are addresses and account names; the
52 // sweep drops the ones whose window has passed, and a table past this size is
53 // cleared rather than grown, because the alternative is memory an anonymous
54 // client chooses.
55 maxLimitKeys = 1 << 16
25343521
Keenan Nemetz
2d ago
56 // limitSweepEvery bounds how long expired keys can sit in the table before a
57 // sweep. Without it a table below the cap is only swept when it becomes full,
58 // so a client can grow it one key at a time for as long as it likes.
59 limitSweepEvery = 1024
60 // maxRateKeyBytes bounds what one key can add to the table. Account names and
61 // addresses are short; a key longer than this is hashed so a client-supplied
62 // string cannot decide how much memory the map holds.
63 maxRateKeyBytes = 256
64 // maxClientAddressBytes bounds the proxy header before it is used as a key or
65 // written to the log. The header is client-influenced when a proxy does not
66 // replace it, and the rate limit is not the place to trust its length.
67 maxClientAddressBytes = 64
edf9db6e
Keenan Nemetz
12d ago
68 )
69
70 // rateLimiter counts events per key inside one window.
71 type rateLimiter struct {
72 mu sync.Mutex
73 limit int
74 window time.Duration
75 now func() time.Time
76 m map[string]*rateEntry
77 }
78
79 type rateEntry struct {
80 count int
81 until time.Time
82 }
83
84 func newRateLimiter(limit int, window time.Duration) *rateLimiter {
85 return &rateLimiter{limit: limit, window: window, now: time.Now, m: map[string]*rateEntry{}}
86 }
87
88 // allow records one attempt for key and reports whether it is inside the budget.
89 // When it is not, the second result is how long the caller should wait.
90 func (l *rateLimiter) allow(key string) (bool, time.Duration) {
91 if l == nil || l.limit <= 0 || key == "" {
92 return true, 0
93 }
25343521
Keenan Nemetz
2d ago
94 key = rateKey(key)
edf9db6e
Keenan Nemetz
12d ago
95 now := l.now().UTC()
96 l.mu.Lock()
97 defer l.mu.Unlock()
98 if e, ok := l.m[key]; ok && now.Before(e.until) {
99 if e.count >= l.limit {
100 return false, e.until.Sub(now)
101 }
102 e.count++
103 return true, 0
104 }
25343521
Keenan Nemetz
2d ago
105 if len(l.m) >= maxLimitKeys || (len(l.m) > 0 && len(l.m)%limitSweepEvery == 0) {
edf9db6e
Keenan Nemetz
12d ago
106 l.sweep(now)
107 }
108 l.m[key] = &rateEntry{count: 1, until: now.Add(l.window)}
109 return true, 0
110 }
111
112 // sweep drops expired entries. It is called when the table is full, so the work is
113 // proportional to the number of keys a client made us remember.
114 func (l *rateLimiter) sweep(now time.Time) {
115 for k, e := range l.m {
116 if !now.Before(e.until) {
117 delete(l.m, k)
118 }
119 }
120 if len(l.m) >= maxLimitKeys {
121 // Still full of live windows: start over. A client that forced this has
122 // already spent the requests, and the alternative is unbounded memory.
123 l.m = map[string]*rateEntry{}
124 }
125 }
126
127 // authLimits is the set of budgets the authentication handlers share.
128 type authLimits struct {
129 beginAddress *rateLimiter
130 beginAccount *rateLimiter
131 finish *rateLimiter
132 register *rateLimiter
e5542ac6
Keenan Nemetz
12d ago
133 // write bounds an untrusted account's repository writes for an hour. It is
134 // keyed by the account rather than the address, because the account is what a
135 // write is attributed to and because one person may use two browsers.
9e8cc155
Keenan Nemetz
12d ago
136 write *rateLimiter
edf9db6e
Keenan Nemetz
12d ago
137 }
138
9e8cc155
Keenan Nemetz
12d ago
139 func newAuthLimits(writesPerHour int) authLimits {
140 if writesPerHour <= 0 {
e5542ac6
Keenan Nemetz
12d ago
141 writesPerHour = defaultContributorWritesPerHour
9e8cc155
Keenan Nemetz
12d ago
142 }
edf9db6e
Keenan Nemetz
12d ago
143 return authLimits{
144 beginAddress: newRateLimiter(authBeginsPerAddress, authLimitWindow),
145 beginAccount: newRateLimiter(authBeginsPerAccount, authLimitWindow),
146 finish: newRateLimiter(authFinishesPerAddress, authLimitWindow),
147 register: newRateLimiter(registrationsPerAddress, authLimitWindow),
9e8cc155
Keenan Nemetz
12d ago
148 write: newRateLimiter(writesPerHour, time.Hour),
149 }
150 }
151
152 // limitForm is limit for a form post: the refusal is a plain-text answer a browser
153 // form can display, not a JSON error.
154 func (w *Web) limitForm(rw http.ResponseWriter, r *http.Request, l *rateLimiter, key string) bool {
155 ok, wait := l.allow(key)
156 if ok {
157 return true
158 }
159 seconds := int(wait.Seconds() + 0.5)
160 if seconds < 1 {
161 seconds = 1
162 }
163 rw.Header().Set("Retry-After", strconv.Itoa(seconds))
164 if w.Logger != nil {
165 w.Logger("write limit: %s %s by %s", r.Method, loggablePath(r.URL.Path), key)
edf9db6e
Keenan Nemetz
12d ago
166 }
9e8cc155
Keenan Nemetz
12d ago
167 http.Error(rw, fmt.Sprintf("you are writing too quickly; try again in %d second(s)", seconds),
168 http.StatusTooManyRequests)
169 return false
edf9db6e
Keenan Nemetz
12d ago
170 }
171
25343521
Keenan Nemetz
2d ago
172 // rateKey bounds the key that reaches the map. A short key is stored as written
173 // so the common case stays readable in a test or a trace; a longer one is hashed
174 // to a fixed size so its length cannot become the table's memory use.
175 func rateKey(key string) string {
176 if len(key) <= maxRateKeyBytes {
177 return key
178 }
179 sum := sha256.Sum256([]byte(key))
180 return "sha256:" + hex.EncodeToString(sum[:])
181 }
182
edf9db6e
Keenan Nemetz
12d ago
183 // clientAddress is the key the address budgets count under. Behind a trusted proxy
184 // the configured header is the client; without one it is the connection. The same
185 // header drives the anonymous Git transport's per-client limit, so an instance
186 // with a proxy sets it once and both surfaces follow.
187 func (w *Web) clientAddress(r *http.Request) string {
188 if w.Cfg != nil {
189 if header := strings.TrimSpace(w.Cfg.GitHTTPClientIPHeader); header != "" {
190 if v := strings.TrimSpace(r.Header.Get(header)); v != "" {
191 // X-Forwarded-For grows left to right and the leftmost entry is the
192 // client; a value that is a list is split rather than used whole.
193 if i := strings.IndexByte(v, ','); i >= 0 {
194 v = strings.TrimSpace(v[:i])
195 }
25343521
Keenan Nemetz
2d ago
196 if len(v) > maxClientAddressBytes {
197 v = v[:maxClientAddressBytes]
198 }
edf9db6e
Keenan Nemetz
12d ago
199 return v
200 }
201 }
202 }
203 host, _, err := net.SplitHostPort(r.RemoteAddr)
204 if err != nil {
205 return r.RemoteAddr
206 }
207 return host
208 }
209
210 // limit answers 429 when the budget for key is used up and reports whether the
211 // handler should continue. The message says how long to wait, because a limit
212 // nobody can see is a wall.
213 func (w *Web) limit(rw http.ResponseWriter, r *http.Request, l *rateLimiter, key string) bool {
214 ok, wait := l.allow(key)
215 if ok {
216 return true
217 }
218 seconds := int(wait.Seconds() + 0.5)
219 if seconds < 1 {
220 seconds = 1
221 }
222 rw.Header().Set("Retry-After", strconv.Itoa(seconds))
223 if w.Logger != nil {
224 w.Logger("auth limit: %s %s from %s", r.Method, loggablePath(r.URL.Path), w.clientAddress(r))
225 }
226 w.noStore(rw)
227 writeJSONError(rw, http.StatusTooManyRequests,
228 fmt.Sprintf("too many attempts; try again in %d second(s)", seconds))
229 return false
230 }