122 lines
3.0 KiB
Go
122 lines
3.0 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"net"
|
||
|
|
"net/http"
|
||
|
|
"sync"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
// limiter is a fixed-window counter, per key, held in memory.
|
||
|
|
//
|
||
|
|
// In memory is the right scope here: verdande is one process with one SQLite file
|
||
|
|
// beneath it, so there is no second instance for a shared counter to coordinate
|
||
|
|
// with. Resetting the limits on restart is a real weakness, and an acceptable one —
|
||
|
|
// this exists to make online password guessing impractical, not to survive a
|
||
|
|
// determined attacker who can also restart the server.
|
||
|
|
type limiter struct {
|
||
|
|
mu sync.Mutex
|
||
|
|
windows map[string]*window
|
||
|
|
limit int
|
||
|
|
period time.Duration
|
||
|
|
}
|
||
|
|
|
||
|
|
type window struct {
|
||
|
|
count int
|
||
|
|
until time.Time
|
||
|
|
}
|
||
|
|
|
||
|
|
func newLimiter(limit int, period time.Duration) *limiter {
|
||
|
|
l := &limiter{windows: map[string]*window{}, limit: limit, period: period}
|
||
|
|
go l.sweep()
|
||
|
|
return l
|
||
|
|
}
|
||
|
|
|
||
|
|
// allow records an attempt and reports whether it may proceed, with how long to
|
||
|
|
// wait if not.
|
||
|
|
func (l *limiter) allow(key string) (bool, time.Duration) {
|
||
|
|
l.mu.Lock()
|
||
|
|
defer l.mu.Unlock()
|
||
|
|
|
||
|
|
now := time.Now()
|
||
|
|
w, ok := l.windows[key]
|
||
|
|
if !ok || now.After(w.until) {
|
||
|
|
l.windows[key] = &window{count: 1, until: now.Add(l.period)}
|
||
|
|
return true, 0
|
||
|
|
}
|
||
|
|
if w.count >= l.limit {
|
||
|
|
return false, time.Until(w.until)
|
||
|
|
}
|
||
|
|
w.count++
|
||
|
|
return true, 0
|
||
|
|
}
|
||
|
|
|
||
|
|
// reset clears a key. Called after a successful login, so somebody who mistyped
|
||
|
|
// their password four times is not still rate-limited once they get it right.
|
||
|
|
func (l *limiter) reset(key string) {
|
||
|
|
l.mu.Lock()
|
||
|
|
delete(l.windows, key)
|
||
|
|
l.mu.Unlock()
|
||
|
|
}
|
||
|
|
|
||
|
|
// sweep drops expired windows. Without it the map is a slow memory leak keyed by
|
||
|
|
// every address that ever failed a login.
|
||
|
|
func (l *limiter) sweep() {
|
||
|
|
for range time.Tick(5 * time.Minute) {
|
||
|
|
l.mu.Lock()
|
||
|
|
now := time.Now()
|
||
|
|
for k, w := range l.windows {
|
||
|
|
if now.After(w.until) {
|
||
|
|
delete(l.windows, k)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
l.mu.Unlock()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// rateLimit guards an endpoint by client address.
|
||
|
|
func (s *Server) rateLimit(l *limiter, next http.Handler) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
if ok, retry := l.allow(clientIP(r)); !ok {
|
||
|
|
w.Header().Set("Retry-After", retryAfterSeconds(retry))
|
||
|
|
writeError(w, http.StatusTooManyRequests, CodeRateLimited,
|
||
|
|
"too many attempts; wait a moment and try again")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
next.ServeHTTP(w, r)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
// clientIP is the address the middleware chain has already resolved through any
|
||
|
|
// proxy headers — behind a Cloudflare Tunnel that is the caller, not the tunnel.
|
||
|
|
// The port is dropped so repeated attempts from one machine share a bucket.
|
||
|
|
func clientIP(r *http.Request) string {
|
||
|
|
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||
|
|
if err != nil {
|
||
|
|
return r.RemoteAddr
|
||
|
|
}
|
||
|
|
return host
|
||
|
|
}
|
||
|
|
|
||
|
|
func retryAfterSeconds(d time.Duration) string {
|
||
|
|
secs := int(d.Seconds())
|
||
|
|
if secs < 1 {
|
||
|
|
secs = 1
|
||
|
|
}
|
||
|
|
return itoa(secs)
|
||
|
|
}
|
||
|
|
|
||
|
|
func itoa(n int) string {
|
||
|
|
if n == 0 {
|
||
|
|
return "0"
|
||
|
|
}
|
||
|
|
var b [20]byte
|
||
|
|
i := len(b)
|
||
|
|
for n > 0 {
|
||
|
|
i--
|
||
|
|
b[i] = byte('0' + n%10)
|
||
|
|
n /= 10
|
||
|
|
}
|
||
|
|
return string(b[i:])
|
||
|
|
}
|