153 lines
3.1 KiB
Go
153 lines
3.1 KiB
Go
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
|
||
|
|
}
|