Moonshark/core/sessions/Manager.go
2025-04-09 20:47:22 -05:00

208 lines
4.6 KiB
Go

package sessions
import (
"crypto/rand"
"encoding/base64"
"sync"
"time"
"github.com/valyala/fasthttp"
)
const (
DefaultMaxSessions = 10000
DefaultCookieName = "MoonsharkSID"
DefaultCookiePath = "/"
DefaultMaxAge = 86400 // 1 day in seconds
)
// SessionManager handles multiple sessions
type SessionManager struct {
sessions map[string]*Session
maxSessions int
cookieName string
cookiePath string
cookieDomain string
cookieSecure bool
cookieHTTPOnly bool
cookieMaxAge int
mu sync.RWMutex
}
// NewSessionManager creates a new session manager
func NewSessionManager(maxSessions int) *SessionManager {
if maxSessions <= 0 {
maxSessions = DefaultMaxSessions
}
return &SessionManager{
sessions: make(map[string]*Session, maxSessions),
maxSessions: maxSessions,
cookieName: DefaultCookieName,
cookiePath: DefaultCookiePath,
cookieHTTPOnly: true,
cookieMaxAge: DefaultMaxAge,
}
}
// generateSessionID creates a random session ID
func generateSessionID() string {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return time.Now().String() // Fallback
}
return base64.URLEncoding.EncodeToString(b)
}
// GetSession retrieves a session by ID, or creates a new one if it doesn't exist
func (sm *SessionManager) GetSession(id string) *Session {
// Try to get an existing session
if id != "" {
sm.mu.RLock()
session, exists := sm.sessions[id]
sm.mu.RUnlock()
if exists {
// Check if session is expired
if session.IsExpired() {
sm.mu.Lock()
delete(sm.sessions, id)
sm.mu.Unlock()
} else {
// Update last used time
session.UpdateLastUsed()
return session
}
}
}
// Create a new session
return sm.CreateSession()
}
// CreateSession generates a new session with a unique ID
func (sm *SessionManager) CreateSession() *Session {
id := generateSessionID()
session := NewSession(id, sm.cookieMaxAge)
sm.mu.Lock()
// Enforce session limit - evict LRU if needed
if len(sm.sessions) >= sm.maxSessions {
sm.evictLRU()
}
sm.sessions[id] = session
sm.mu.Unlock()
return session
}
// evictLRU removes the least recently used session
func (sm *SessionManager) evictLRU() {
// Called with mutex already held
if len(sm.sessions) == 0 {
return
}
var oldestID string
var oldestTime time.Time
// Find oldest session
for id, session := range sm.sessions {
if oldestID == "" || session.LastUsed.Before(oldestTime) {
oldestID = id
oldestTime = session.LastUsed
}
}
if oldestID != "" {
delete(sm.sessions, oldestID)
}
}
// DestroySession removes a session
func (sm *SessionManager) DestroySession(id string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.sessions, id)
}
// CleanupExpired removes all expired sessions
func (sm *SessionManager) CleanupExpired() int {
removed := 0
now := time.Now()
sm.mu.Lock()
defer sm.mu.Unlock()
for id, session := range sm.sessions {
if now.After(session.Expiry) {
delete(sm.sessions, id)
removed++
}
}
return removed
}
// SetCookieOptions configures cookie parameters
func (sm *SessionManager) SetCookieOptions(name, path, domain string, secure, httpOnly bool, maxAge int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.cookieName = name
sm.cookiePath = path
sm.cookieDomain = domain
sm.cookieSecure = secure
sm.cookieHTTPOnly = httpOnly
sm.cookieMaxAge = maxAge
}
// GetSessionFromRequest extracts the session from a request
func (sm *SessionManager) GetSessionFromRequest(ctx *fasthttp.RequestCtx) *Session {
cookie := ctx.Request.Header.Cookie(sm.cookieName)
if len(cookie) == 0 {
return sm.CreateSession()
}
return sm.GetSession(string(cookie))
}
// ApplySessionCookie adds the session cookie to the response
func (sm *SessionManager) ApplySessionCookie(ctx *fasthttp.RequestCtx, session *Session) {
cookie := fasthttp.AcquireCookie()
defer fasthttp.ReleaseCookie(cookie)
cookie.SetKey(sm.cookieName)
cookie.SetValue(session.ID)
cookie.SetPath(sm.cookiePath)
cookie.SetHTTPOnly(sm.cookieHTTPOnly)
cookie.SetMaxAge(sm.cookieMaxAge)
if sm.cookieDomain != "" {
cookie.SetDomain(sm.cookieDomain)
}
cookie.SetSecure(sm.cookieSecure)
ctx.Response.Header.SetCookie(cookie)
}
// CookieOptions returns the cookie options for this session manager
func (sm *SessionManager) CookieOptions() map[string]any {
sm.mu.RLock()
defer sm.mu.RUnlock()
return map[string]any{
"name": sm.cookieName,
"path": sm.cookiePath,
"domain": sm.cookieDomain,
"secure": sm.cookieSecure,
"http_only": sm.cookieHTTPOnly,
"max_age": sm.cookieMaxAge,
}
}
// GlobalSessionManager is the default session manager instance
var GlobalSessionManager = NewSessionManager(DefaultMaxSessions)