From 46caf94ae18ceb20c4b4bd946b4c2bce76b36a01 Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sat, 5 Apr 2025 22:26:01 -0500 Subject: [PATCH] sessions 2 --- core/sessions/Manager.go | 132 +++++++++++++++ core/sessions/Session.go | 3 +- core/sessions/SessionManager.go | 287 -------------------------------- 3 files changed, 134 insertions(+), 288 deletions(-) create mode 100644 core/sessions/Manager.go delete mode 100644 core/sessions/SessionManager.go diff --git a/core/sessions/Manager.go b/core/sessions/Manager.go new file mode 100644 index 0000000..8be2a11 --- /dev/null +++ b/core/sessions/Manager.go @@ -0,0 +1,132 @@ +package sessions + +import ( + "crypto/rand" + "encoding/base64" + "sync" + "time" + + "github.com/VictoriaMetrics/fastcache" + "github.com/goccy/go-json" +) + +const ( + // Default settings + DefaultMaxSize = 100 * 1024 * 1024 // 100MB default cache size + DefaultCookieName = "MSESSID" + DefaultCookiePath = "/" + DefaultMaxAge = 86400 // 1 day in seconds +) + +// SessionManager handles multiple sessions using fastcache for storage +type SessionManager struct { + cache *fastcache.Cache + cookieName string + cookiePath string + cookieDomain string + cookieSecure bool + cookieHTTPOnly bool + cookieMaxAge int + mu sync.RWMutex // Only for cookie settings +} + +// NewSessionManager creates a new session manager with optional cache size +func NewSessionManager(maxSize ...int) *SessionManager { + size := DefaultMaxSize + if len(maxSize) > 0 && maxSize[0] > 0 { + size = maxSize[0] + } + + return &SessionManager{ + cache: fastcache.New(size), + cookieName: DefaultCookieName, + cookiePath: DefaultCookiePath, + cookieHTTPOnly: true, + cookieMaxAge: DefaultMaxAge, + } +} + +// generateSessionID creates a cryptographically secure 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 { + // Check if session exists + data := sm.cache.Get(nil, []byte(id)) + + if len(data) > 0 { + // Session exists, unmarshal it + session := &Session{} + if err := json.Unmarshal(data, session); err == nil { + // Update last accessed time + session.UpdatedAt = time.Now() + + // Store back with updated timestamp + updatedData, _ := json.Marshal(session) + sm.cache.Set([]byte(id), updatedData) + + return session + } + } + + // Create new session + session := NewSession(id) + data, _ = json.Marshal(session) + sm.cache.Set([]byte(id), data) + + return session +} + +// CreateSession generates a new session with a unique ID +func (sm *SessionManager) CreateSession() *Session { + id := generateSessionID() + + // Create new session + session := NewSession(id) + data, _ := json.Marshal(session) + sm.cache.Set([]byte(id), data) + + return session +} + +// DestroySession removes a session +func (sm *SessionManager) DestroySession(id string) { + sm.cache.Del([]byte(id)) +} + +// 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, + } +} + +// 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() diff --git a/core/sessions/Session.go b/core/sessions/Session.go index efd3775..fa18609 100644 --- a/core/sessions/Session.go +++ b/core/sessions/Session.go @@ -1,10 +1,11 @@ package sessions import ( - "encoding/json" "errors" "sync" "time" + + "github.com/goccy/go-json" ) const ( diff --git a/core/sessions/SessionManager.go b/core/sessions/SessionManager.go deleted file mode 100644 index 28f68cb..0000000 --- a/core/sessions/SessionManager.go +++ /dev/null @@ -1,287 +0,0 @@ -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()