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)