Files
plumber/internal/web/throttle.go
T
codegirl007 59513ab75e Harden auth: setup secret, throttling, session destroy, secure cookies.
Replace username-based admin bootstrap with a one-time setup secret, rate-limit login/register, equalize login bcrypt timing, cap passwords at 72 bytes, destroy sessions on logout, and require Secure cookies when PORT is set.
2026-08-22 11:47:42 -07:00

84 lines
1.6 KiB
Go

package web
import (
"net"
"net/http"
"strings"
"sync"
"time"
)
// throttle is a simple sliding-window rate limiter for auth endpoints.
type throttle struct {
mu sync.Mutex
hits map[string][]time.Time
limit int
window time.Duration
}
func newThrottle(limit int, window time.Duration) *throttle {
return &throttle{
hits: map[string][]time.Time{},
limit: limit,
window: window,
}
}
func (t *throttle) allow(key string) bool {
if t == nil || key == "" {
return true
}
t.mu.Lock()
defer t.mu.Unlock()
now := time.Now()
cutoff := now.Add(-t.window)
xs := t.hits[key]
n := 0
for _, ts := range xs {
if ts.After(cutoff) {
xs[n] = ts
n++
}
}
xs = xs[:n]
if len(xs) >= t.limit {
t.hits[key] = xs
return false
}
t.hits[key] = append(xs, now)
return true
}
func (s *Server) clientIP(r *http.Request) string {
if s.cfg.TrustProxy {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
return strings.TrimSpace(strings.Split(xff, ",")[0])
}
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}
func authTooMany(w http.ResponseWriter) {
http.Error(w, "too many attempts; try again later", http.StatusTooManyRequests)
}
func (s *Server) allowLoginAttempt(w http.ResponseWriter, r *http.Request, usernameKey string) bool {
if !s.loginIP.allow(s.clientIP(r)) || !s.loginUser.allow(usernameKey) {
authTooMany(w)
return false
}
return true
}
func (s *Server) allowRegisterAttempt(w http.ResponseWriter, r *http.Request) bool {
if !s.registerIP.allow(s.clientIP(r)) {
authTooMany(w)
return false
}
return true
}