package sessions import ( "Moonshark/core/utils/logger" "crypto/rand" "encoding/base64" "sync" "time" "github.com/VictoriaMetrics/fastcache" "github.com/goccy/go-json" "github.com/valyala/fasthttp" ) const ( // Default settings DefaultMaxSize = 100 * 1024 * 1024 // 100MB default cache size DefaultCookieName = "MoonsharkSID" 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 { logger.Debug("Getting session %s", id) // Session exists, unmarshal it session := &Session{} if err := json.Unmarshal(data, session); err == nil { // Initialize mutex properly session.mu = sync.RWMutex{} // 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 } } logger.Debug("Session doesn't exist; creating it") // 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() session := NewSession(id) data, _ := json.Marshal(session) sm.cache.Set([]byte(id), data) return session } // SaveSession persists a session back to the cache func (sm *SessionManager) SaveSession(session *Session) { data, _ := json.Marshal(session) sm.cache.Set([]byte(session.ID), data) } // 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 } // GetSessionFromRequest extracts the session from a request context func (sm *SessionManager) GetSessionFromRequest(ctx *fasthttp.RequestCtx) *Session { cookie := ctx.Request.Header.Cookie(sm.cookieName) if len(cookie) == 0 { // No session cookie, create a new session return sm.CreateSession() } // Session cookie exists, get the session return sm.GetSession(string(cookie)) } // SaveSessionToResponse adds the session cookie to an HTTP response func (sm *SessionManager) ApplySessionCookie(ctx *fasthttp.RequestCtx, session *Session) { cookie := fasthttp.AcquireCookie() defer fasthttp.ReleaseCookie(cookie) sm.mu.RLock() 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) sm.mu.RUnlock() ctx.Response.Header.SetCookie(cookie) } // GlobalSessionManager is the default session manager instance var GlobalSessionManager = NewSessionManager()