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.
84 lines
1.6 KiB
Go
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
|
|
}
|