package web import ( "crypto/rand" "crypto/subtle" "encoding/hex" "net" "net/http" "sync" "time" ) const ( sessionCookieName = "opengfw_session" sessionTTL = 7 * 24 * time.Hour maxLoginFailures = 8 loginBanDuration = 5 * time.Minute ) type authenticator struct { secret string mu sync.Mutex sessions map[string]time.Time // token -> expiry failures map[string]*failureRecord } type failureRecord struct { count int until time.Time } func newAuthenticator(secret string) *authenticator { return &authenticator{ secret: secret, sessions: make(map[string]time.Time), failures: make(map[string]*failureRecord), } } // RandomSecret generates a secret to be used when the user did not set one. func RandomSecret() string { b := make([]byte, 12) if _, err := rand.Read(b); err != nil { return "opengfw" } return hex.EncodeToString(b) } func newToken() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { return "" } return hex.EncodeToString(b) } // login verifies the password and returns a new session token. func (a *authenticator) login(remoteAddr, password string) (string, time.Time, error) { ip := hostOnly(remoteAddr) a.mu.Lock() defer a.mu.Unlock() now := time.Now() if rec, ok := a.failures[ip]; ok && rec.count >= maxLoginFailures && now.Before(rec.until) { return "", time.Time{}, errTooManyAttempts } if subtle.ConstantTimeCompare([]byte(password), []byte(a.secret)) != 1 { rec, ok := a.failures[ip] if !ok || now.After(rec.until) { rec = &failureRecord{} a.failures[ip] = rec } rec.count++ rec.until = now.Add(loginBanDuration) return "", time.Time{}, errBadCredentials } delete(a.failures, ip) token := newToken() if token == "" { return "", time.Time{}, errInternal } expiry := now.Add(sessionTTL) a.sessions[token] = expiry a.gcLocked(now) return token, expiry, nil } func (a *authenticator) logout(token string) { if token == "" { return } a.mu.Lock() delete(a.sessions, token) a.mu.Unlock() } func (a *authenticator) valid(token string) bool { if token == "" { return false } a.mu.Lock() defer a.mu.Unlock() expiry, ok := a.sessions[token] if !ok { return false } if time.Now().After(expiry) { delete(a.sessions, token) return false } return true } func (a *authenticator) gcLocked(now time.Time) { for t, exp := range a.sessions { if now.After(exp) { delete(a.sessions, t) } } for ip, rec := range a.failures { if now.After(rec.until) { delete(a.failures, ip) } } } // bearerToken returns the token carried by the Authorization header, if any. func bearerToken(r *http.Request) string { const prefix = "Bearer " h := r.Header.Get("Authorization") if len(h) > len(prefix) && h[:len(prefix)] == prefix { return h[len(prefix):] } return "" } // cookieToken returns the token carried by the session cookie, if any. func cookieToken(r *http.Request) string { c, err := r.Cookie(sessionCookieName) if err != nil { return "" } return c.Value } func hostOnly(addr string) string { host, _, err := net.SplitHostPort(addr) if err != nil { return addr } return host }