Refactor load testing scripts to support Bash and PowerShell
CI / test (push) Successful in 2m50s
CI / docker (push) Successful in 5m12s

- 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.
This commit is contained in:
zwitschi committed 2026-09-02 19:47:34 +02:00
1 parent 9f60087a4c
commit 33b6296c5e
82 files changed
+19682 -21133

No files matched your search

+206 -206
View File
@@ -1,207 +1,207 @@
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)
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)
}