288 lines
7.0 KiB
Go
288 lines
7.0 KiB
Go
package sessions
|
|
|
|
import (
|
|
"container/list"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// Default limits
|
|
DefaultMaxMemory int64 = 100 * 1024 * 1024 // 100MB default
|
|
DefaultMaxSessions int = 10000 // 10K sessions max
|
|
)
|
|
|
|
// SessionManager handles multiple sessions
|
|
type SessionManager struct {
|
|
sessions map[string]*Session
|
|
mu sync.RWMutex
|
|
cookieName string
|
|
cookiePath string
|
|
cookieDomain string
|
|
cookieSecure bool
|
|
cookieHTTPOnly bool
|
|
cookieMaxAge int
|
|
gcInterval time.Duration
|
|
|
|
// Memory management
|
|
maxMemory int64 // Maximum memory limit in bytes
|
|
currentMemory int64 // Current estimated memory usage
|
|
maxSessions int // Maximum number of sessions
|
|
lruList *list.List // For tracking session access order
|
|
lruMap map[string]*list.Element // For fast lookups
|
|
}
|
|
|
|
// NewSessionManager creates a new session manager
|
|
func NewSessionManager() *SessionManager {
|
|
sm := &SessionManager{
|
|
sessions: make(map[string]*Session),
|
|
cookieName: "MSESSID",
|
|
cookiePath: "/",
|
|
cookieHTTPOnly: true,
|
|
cookieMaxAge: 86400, // 1 day
|
|
gcInterval: time.Hour,
|
|
maxMemory: DefaultMaxMemory,
|
|
maxSessions: DefaultMaxSessions,
|
|
lruList: list.New(),
|
|
lruMap: make(map[string]*list.Element),
|
|
}
|
|
|
|
// Start the garbage collector
|
|
go sm.startGC()
|
|
|
|
return sm
|
|
}
|
|
|
|
// trackAccess moves a session to the front of the LRU list
|
|
func (sm *SessionManager) trackAccess(id string) {
|
|
// No need for sm.mu lock here as this is always called from
|
|
// methods that already hold the lock
|
|
|
|
if elem, ok := sm.lruMap[id]; ok {
|
|
sm.lruList.MoveToFront(elem)
|
|
return
|
|
}
|
|
|
|
// If not in list, add it
|
|
elem := sm.lruList.PushFront(id)
|
|
sm.lruMap[id] = elem
|
|
}
|
|
|
|
// evictOldest removes the least recently used session
|
|
func (sm *SessionManager) evictOldest() {
|
|
// No need for sm.mu lock here as this is always called from
|
|
// methods that already hold the lock
|
|
|
|
if sm.lruList.Len() == 0 {
|
|
return
|
|
}
|
|
|
|
// Get the oldest session
|
|
elem := sm.lruList.Back()
|
|
id := elem.Value.(string)
|
|
|
|
// Remove from list and map
|
|
sm.lruList.Remove(elem)
|
|
delete(sm.lruMap, id)
|
|
|
|
// Remove session and update memory
|
|
if session, exists := sm.sessions[id]; exists {
|
|
sessionSize := int64(session.GetTotalSize() + 256) // Base size + data
|
|
atomic.AddInt64(&sm.currentMemory, -sessionSize)
|
|
delete(sm.sessions, id)
|
|
}
|
|
}
|
|
|
|
// ensureCapacity evicts sessions if we're over the limit
|
|
func (sm *SessionManager) ensureCapacity() {
|
|
// No lock needed - called from methods that have the lock
|
|
|
|
// Check session count limit
|
|
for len(sm.sessions) >= sm.maxSessions {
|
|
sm.evictOldest()
|
|
}
|
|
|
|
// Check memory limit
|
|
for atomic.LoadInt64(&sm.currentMemory) > sm.maxMemory && sm.lruList.Len() > 0 {
|
|
sm.evictOldest()
|
|
}
|
|
}
|
|
|
|
// generateSessionID creates a cryptographically secure random session ID
|
|
func (sm *SessionManager) 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 {
|
|
sm.mu.RLock()
|
|
session, exists := sm.sessions[id]
|
|
sm.mu.RUnlock()
|
|
|
|
if exists {
|
|
// Update LRU status
|
|
sm.mu.Lock()
|
|
sm.trackAccess(id)
|
|
sm.mu.Unlock()
|
|
return session
|
|
}
|
|
|
|
// Create new session if it doesn't exist
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
// Double check to avoid race conditions
|
|
if session, exists = sm.sessions[id]; exists {
|
|
sm.trackAccess(id)
|
|
return session
|
|
}
|
|
|
|
// Check capacity before creating new session
|
|
sm.ensureCapacity()
|
|
|
|
session = NewSession(id)
|
|
sm.sessions[id] = session
|
|
sm.trackAccess(id)
|
|
|
|
// Update memory tracking (estimate base session size + initial map)
|
|
sessionSize := int64(256)
|
|
atomic.AddInt64(&sm.currentMemory, sessionSize)
|
|
|
|
return session
|
|
}
|
|
|
|
// CreateSession generates a new session with a unique ID
|
|
func (sm *SessionManager) CreateSession() *Session {
|
|
id := sm.generateSessionID()
|
|
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
// Ensure we have capacity
|
|
sm.ensureCapacity()
|
|
|
|
session := NewSession(id)
|
|
sm.sessions[id] = session
|
|
sm.trackAccess(id)
|
|
|
|
// Update memory tracking (estimate base session size + initial map)
|
|
sessionSize := int64(256)
|
|
atomic.AddInt64(&sm.currentMemory, sessionSize)
|
|
|
|
return session
|
|
}
|
|
|
|
// DestroySession removes a session
|
|
func (sm *SessionManager) DestroySession(id string) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
if session, exists := sm.sessions[id]; exists {
|
|
// Update memory tracking
|
|
sessionSize := int64(session.GetTotalSize() + 256) // Base size + data
|
|
atomic.AddInt64(&sm.currentMemory, -sessionSize)
|
|
|
|
// Remove from tracking structures
|
|
delete(sm.sessions, id)
|
|
if elem, ok := sm.lruMap[id]; ok {
|
|
sm.lruList.Remove(elem)
|
|
delete(sm.lruMap, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SetMaxMemory sets the maximum memory limit
|
|
func (sm *SessionManager) SetMaxMemory(bytes int64) {
|
|
atomic.StoreInt64(&sm.maxMemory, bytes)
|
|
}
|
|
|
|
// SetMaxSessions sets the maximum number of sessions
|
|
func (sm *SessionManager) SetMaxSessions(max int) {
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
sm.maxSessions = max
|
|
|
|
// If we're over the new limit, evict oldest sessions
|
|
sm.ensureCapacity()
|
|
}
|
|
|
|
// startGC starts the garbage collector to clean up expired sessions
|
|
func (sm *SessionManager) startGC() {
|
|
ticker := time.NewTicker(sm.gcInterval)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
sm.gc()
|
|
}
|
|
}
|
|
|
|
// gc removes expired sessions (inactive for 24 hours)
|
|
func (sm *SessionManager) gc() {
|
|
expiry := time.Now().Add(-24 * time.Hour)
|
|
|
|
sm.mu.Lock()
|
|
defer sm.mu.Unlock()
|
|
|
|
for id, session := range sm.sessions {
|
|
session.mu.RLock()
|
|
lastUpdated := session.UpdatedAt
|
|
session.mu.RUnlock()
|
|
|
|
if lastUpdated.Before(expiry) {
|
|
// Update memory tracking
|
|
sessionSize := int64(session.GetTotalSize() + 256)
|
|
atomic.AddInt64(&sm.currentMemory, -sessionSize)
|
|
|
|
// Remove from tracking structures
|
|
delete(sm.sessions, id)
|
|
if elem, ok := sm.lruMap[id]; ok {
|
|
sm.lruList.Remove(elem)
|
|
delete(sm.lruMap, id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GetSessionCount returns the number of active sessions
|
|
func (sm *SessionManager) GetSessionCount() int {
|
|
sm.mu.RLock()
|
|
defer sm.mu.RUnlock()
|
|
return len(sm.sessions)
|
|
}
|
|
|
|
// CookieOptions returns the cookie options for this session manager
|
|
func (sm *SessionManager) CookieOptions() map[string]any {
|
|
return map[string]any{
|
|
"name": sm.cookieName,
|
|
"path": sm.cookiePath,
|
|
"domain": sm.cookieDomain,
|
|
"secure": sm.cookieSecure,
|
|
"http_only": sm.cookieHTTPOnly,
|
|
"max_age": sm.cookieMaxAge,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// GlobalSessionManager is the default session manager instance
|
|
var GlobalSessionManager = NewSessionManager()
|