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()