- Updated README.md to reflect changes in load testing commands and added usage examples for Bash scripts. - Converted PowerShell load testing scripts to Bash equivalents for better cross-platform compatibility. - Added new Bash scripts: run-loadtests.sh, summarize-artillery.sh, and summarize-k6.sh to mirror existing PowerShell functionality. - Updated admin-api.js and ws-chat.yml for consistency and improved performance metrics reporting. - Enhanced error handling and output formatting in summary scripts for better readability.
207 lines
5.4 KiB
Go
207 lines
5.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// SessionStore manages admin authentication sessions.
|
|
type SessionStore struct {
|
|
mu sync.Mutex
|
|
sessions map[string]time.Time
|
|
}
|
|
|
|
// NewSessionStore creates a new session store.
|
|
func NewSessionStore() *SessionStore {
|
|
return &SessionStore{
|
|
sessions: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
// GenerateToken creates a cryptographically random 64-char hex session token.
|
|
func GenerateToken() (string, error) {
|
|
buf := make([]byte, 32)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(buf), nil
|
|
}
|
|
|
|
// Validate checks if a session token is valid and not expired.
|
|
// Returns true if valid, false otherwise. Expired sessions are cleaned up.
|
|
func (s *SessionStore) Validate(token string) bool {
|
|
if token == "" {
|
|
return false
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
expiresAt, ok := s.sessions[token]
|
|
if !ok {
|
|
return false
|
|
}
|
|
if time.Now().After(expiresAt) {
|
|
delete(s.sessions, token)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Add stores a session token with expiry.
|
|
func (s *SessionStore) Add(token string, expiresAt time.Time) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.sessions[token] = expiresAt
|
|
}
|
|
|
|
// Remove deletes a session token.
|
|
func (s *SessionStore) Remove(token string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
delete(s.sessions, token)
|
|
}
|
|
|
|
// CountActive returns the number of unexpired sessions.
|
|
func (s *SessionStore) CountActive() int {
|
|
now := time.Now()
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
|
|
for token, expiresAt := range s.sessions {
|
|
if now.After(expiresAt) {
|
|
delete(s.sessions, token)
|
|
}
|
|
}
|
|
return len(s.sessions)
|
|
}
|
|
|
|
// CookieSecure determines if the session cookie should have Secure flag.
|
|
func CookieSecure(r *http.Request) bool {
|
|
if forwardedProto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto")); forwardedProto != "" {
|
|
return strings.EqualFold(forwardedProto, "https")
|
|
}
|
|
return r.TLS != nil
|
|
}
|
|
|
|
// SameSiteForUserCookie returns the SameSite policy for the viewer session cookie.
|
|
// Over HTTPS it returns SameSite=None so the cookie is sent cross-site from the
|
|
// Workers-served SPA; over plain HTTP (local dev) it falls back to Lax.
|
|
func SameSiteForUserCookie(r *http.Request) http.SameSite {
|
|
if CookieSecure(r) {
|
|
return http.SameSiteNoneMode
|
|
}
|
|
return http.SameSiteLaxMode
|
|
}
|
|
|
|
// SetCookie writes a session cookie with the given SameSite policy.
|
|
// SameSite=None is only valid together with Secure, which is enforced here.
|
|
func SetCookie(w http.ResponseWriter, r *http.Request, cookieName, token string, expiresAt time.Time, sameSite http.SameSite) {
|
|
secure := CookieSecure(r)
|
|
if sameSite == http.SameSiteNoneMode {
|
|
secure = true
|
|
}
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieName,
|
|
Value: token,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: sameSite,
|
|
Secure: secure,
|
|
Expires: expiresAt,
|
|
MaxAge: int(time.Until(expiresAt).Seconds()),
|
|
})
|
|
}
|
|
|
|
// ClearCookie removes a session cookie with the given SameSite policy.
|
|
func ClearCookie(w http.ResponseWriter, r *http.Request, cookieName string, sameSite http.SameSite) {
|
|
secure := CookieSecure(r)
|
|
if sameSite == http.SameSiteNoneMode {
|
|
secure = true
|
|
}
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieName,
|
|
Value: "",
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: sameSite,
|
|
Secure: secure,
|
|
Expires: time.Unix(0, 0),
|
|
MaxAge: -1,
|
|
})
|
|
}
|
|
|
|
// GetTokenFromCookie extracts the session token from the request cookie.
|
|
func GetTokenFromCookie(r *http.Request, cookieName string) (string, bool) {
|
|
cookie, err := r.Cookie(cookieName)
|
|
if err != nil || cookie.Value == "" {
|
|
return "", false
|
|
}
|
|
return cookie.Value, true
|
|
}
|
|
|
|
// RequireAdmin returns middleware that checks for a valid admin session.
|
|
func RequireAdmin(store *SessionStore, cookieName string) func(http.HandlerFunc) http.HandlerFunc {
|
|
return func(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
token, ok := GetTokenFromCookie(r, cookieName)
|
|
if !ok || !store.Validate(token) {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
next(w, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// UserSessionStore maps user session tokens to usernames with expiry.
|
|
type UserSessionStore struct {
|
|
mu sync.Mutex
|
|
sessions map[string]userSession
|
|
}
|
|
|
|
type userSession struct {
|
|
Username string
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// NewUserSessionStore creates a new user session store.
|
|
func NewUserSessionStore() *UserSessionStore {
|
|
return &UserSessionStore{sessions: make(map[string]userSession)}
|
|
}
|
|
|
|
// Add stores a user session token with username and expiry.
|
|
func (s *UserSessionStore) Add(token, username string, expiresAt time.Time) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.sessions[token] = userSession{Username: username, ExpiresAt: expiresAt}
|
|
}
|
|
|
|
// Username returns the username for a valid, unexpired token.
|
|
func (s *UserSessionStore) Username(token string) (string, bool) {
|
|
if token == "" {
|
|
return "", false
|
|
}
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
sess, ok := s.sessions[token]
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
if time.Now().After(sess.ExpiresAt) {
|
|
delete(s.sessions, token)
|
|
return "", false
|
|
}
|
|
return sess.Username, true
|
|
}
|
|
|
|
// Remove deletes a user session token.
|
|
func (s *UserSessionStore) Remove(token string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
delete(s.sessions, token)
|
|
} |