Files

153 lines
3.1 KiB
Go
Raw Permalink Normal View History

2026-07-27 08:09:11 +08:00
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
}