package psession import ( "crypto/rand" "crypto/sha256" "encoding/hex" "errors" "net/http" "sync" "sync/atomic" "time" "github.com/1Panel-dev/1Panel/core/constant" "github.com/gin-gonic/gin" ) type SessionUser struct { ID string `json:"id"` Role string `json:"role"` Name string `json:"name"` } const SuperAdminSessionUserID = "__super_admin__" const GinContextSessionUserKey = "session_user" type sessionItem struct { CreatedAt time.Time LastActiveAt time.Time CSRFToken string User SessionUser ExpiredAt time.Time } type PSession struct { mu sync.RWMutex sessions map[string]sessionItem cleanupCursor atomic.Uint64 lastFullCleanup time.Time } const maxSessionEntries = 1024 func NewPSession(_ string) *PSession { return &PSession{ sessions: make(map[string]sessionItem), } } func (p *PSession) Get(c *gin.Context) (SessionUser, error) { var result SessionUser sessionID, err := c.Cookie(constant.SessionName) if err != nil || sessionID == "" { return result, errors.New("ErrSessionDataNotFound") } p.mu.RLock() item, ok := p.sessions[sessionID] p.mu.RUnlock() if !ok { return result, errors.New("ErrSessionDataNotFound") } if !item.ExpiredAt.IsZero() && time.Now().After(item.ExpiredAt) { p.mu.Lock() delete(p.sessions, sessionID) p.mu.Unlock() return result, errors.New("ErrSessionDataNotFound") } return item.User, nil } func (p *PSession) Set(c *gin.Context, user SessionUser, secure bool, ttlSeconds int) error { return p.set(c, user, secure, ttlSeconds, false) } func (p *PSession) SetFresh(c *gin.Context, user SessionUser, secure bool, ttlSeconds int) error { return p.set(c, user, secure, ttlSeconds, true) } func (p *PSession) set(c *gin.Context, user SessionUser, secure bool, ttlSeconds int, forceNew bool) error { sessionID, err := c.Cookie(constant.SessionName) if forceNew { if err == nil && sessionID != "" { p.mu.Lock() delete(p.sessions, sessionID) p.mu.Unlock() } sessionID = "" } if err != nil || sessionID == "" { sessionID, err = generateSessionID() if err != nil { return err } } now := time.Now() expiredAt := now.Add(time.Duration(ttlSeconds) * time.Second) createdAt := now csrfToken := "" p.mu.Lock() if existing, ok := p.sessions[sessionID]; ok { if !existing.CreatedAt.IsZero() { createdAt = existing.CreatedAt } csrfToken = existing.CSRFToken } if csrfToken == "" { csrfToken, err = generateSessionID() if err != nil { p.mu.Unlock() return err } } p.sessions[sessionID] = sessionItem{ CreatedAt: createdAt, LastActiveAt: now, CSRFToken: csrfToken, User: user, ExpiredAt: expiredAt, } p.evictOverflowLocked(sessionID) p.mu.Unlock() p.cleanupExpiredOnWrite() c.SetSameSite(http.SameSiteLaxMode) c.SetCookie(constant.SessionName, sessionID, ttlSeconds, "/", "", secure, true) c.SetSameSite(http.SameSiteLaxMode) c.SetCookie(constant.CSRFTokenName, csrfToken, ttlSeconds, "/", "", secure, false) return nil } func (p *PSession) evictOverflowLocked(currentSessionID string) { if maxSessionEntries <= 0 || len(p.sessions) <= maxSessionEntries { return } for len(p.sessions) > maxSessionEntries { oldestID := "" var oldestItem sessionItem for sessionID, item := range p.sessions { if sessionID == currentSessionID { continue } if oldestID == "" || item.CreatedAt.Before(oldestItem.CreatedAt) { oldestID = sessionID oldestItem = item } } if oldestID == "" { return } delete(p.sessions, oldestID) } } func (p *PSession) RefreshIfNeeded(c *gin.Context, user SessionUser, secure bool, ttlSeconds int) (bool, error) { sessionID, err := c.Cookie(constant.SessionName) if err != nil || sessionID == "" { return false, p.Set(c, user, secure, ttlSeconds) } p.mu.RLock() item, ok := p.sessions[sessionID] p.mu.RUnlock() if !ok { return false, p.Set(c, user, secure, ttlSeconds) } if !item.ExpiredAt.IsZero() && time.Now().After(item.ExpiredAt) { p.mu.Lock() delete(p.sessions, sessionID) p.mu.Unlock() return false, errors.New("ErrSessionDataNotFound") } return true, p.Set(c, user, secure, ttlSeconds) } func (p *PSession) Delete(c *gin.Context) error { sessionID, err := c.Cookie(constant.SessionName) if err == nil && sessionID != "" { p.mu.Lock() delete(p.sessions, sessionID) p.mu.Unlock() } return nil } func (p *PSession) CheckCSRFToken(c *gin.Context, token string) bool { sessionID, err := c.Cookie(constant.SessionName) if err != nil || sessionID == "" || token == "" { return false } p.mu.RLock() item, ok := p.sessions[sessionID] p.mu.RUnlock() if !ok { return false } if !item.ExpiredAt.IsZero() && time.Now().After(item.ExpiredAt) { p.mu.Lock() delete(p.sessions, sessionID) p.mu.Unlock() return false } return item.CSRFToken == token } func (p *PSession) DeleteByID(id string) error { if id == "" { return nil } p.mu.Lock() defer p.mu.Unlock() for sessionID, item := range p.sessions { if item.User.ID == id { delete(p.sessions, sessionID) } } return nil } func (p *PSession) DeleteByHash(sessionHash string) error { if sessionHash == "" { return nil } p.mu.Lock() defer p.mu.Unlock() for sessionID := range p.sessions { sum := sha256.Sum256([]byte(sessionID)) if hex.EncodeToString(sum[:]) == sessionHash { delete(p.sessions, sessionID) return nil } } return nil } func (p *PSession) ApplyTimeout(ttlSeconds int) { now := time.Now() ttl := time.Duration(ttlSeconds) * time.Second p.mu.Lock() defer p.mu.Unlock() for sessionID, item := range p.sessions { lastActiveAt := item.LastActiveAt if lastActiveAt.IsZero() { lastActiveAt = item.CreatedAt } expiredAt := lastActiveAt.Add(ttl) if now.After(expiredAt) { delete(p.sessions, sessionID) continue } item.ExpiredAt = expiredAt p.sessions[sessionID] = item } } func (p *PSession) Clean() error { p.mu.Lock() p.sessions = make(map[string]sessionItem) p.lastFullCleanup = time.Time{} p.mu.Unlock() return nil } func generateSessionID() (string, error) { buf := make([]byte, 32) if _, err := rand.Read(buf); err != nil { return "", err } return hex.EncodeToString(buf), nil } func (p *PSession) cleanupExpiredOnWrite() { const ( sampleSize = 32 fullCleanupThreshold = 1024 fullCleanupMinInterval = time.Minute ) now := time.Now() p.mu.RLock() size := len(p.sessions) lastFullCleanup := p.lastFullCleanup p.mu.RUnlock() if size == 0 { return } if size >= fullCleanupThreshold && now.Sub(lastFullCleanup) >= fullCleanupMinInterval { p.cleanupExpiredAll(now) return } p.cleanupExpiredSample(now, sampleSize) } func (p *PSession) cleanupExpiredSample(now time.Time, limit int) { if limit <= 0 { return } p.mu.Lock() defer p.mu.Unlock() total := len(p.sessions) if total == 0 { return } start := int(p.cleanupCursor.Add(uint64(limit)) % uint64(total)) checked := 0 idx := 0 for key, item := range p.sessions { if idx < start { idx++ continue } if !item.ExpiredAt.IsZero() && now.After(item.ExpiredAt) { delete(p.sessions, key) } checked++ idx++ if checked >= limit { break } } if checked < limit { for key, item := range p.sessions { if checked >= limit { break } if !item.ExpiredAt.IsZero() && now.After(item.ExpiredAt) { delete(p.sessions, key) } checked++ } } } func (p *PSession) cleanupExpiredAll(now time.Time) { p.mu.Lock() for key, item := range p.sessions { if !item.ExpiredAt.IsZero() && now.After(item.ExpiredAt) { delete(p.sessions, key) } } p.lastFullCleanup = now p.mu.Unlock() }