package services import ( "context" "crypto/rand" "encoding/hex" "errors" "net/http" "sync" "time" "github.com/rs/zerolog/log" ) var ( ErrSessionIDCollision = errors.New("sessionId collision") ErrUnauthorized = errors.New("unauthorized") ) func generateSessionID() (string, error) { sessionID := make([]byte, 32) //nolint if _, err := rand.Read(sessionID); err != nil { return "", err } return hex.EncodeToString(sessionID), nil } type Session struct { l sync.RWMutex sessionID string expirationTime time.Time } func (s *Session) GenerateCookie() *http.Cookie { s.l.RLock() defer s.l.RUnlock() return &http.Cookie{ Name: "session_id", Value: s.sessionID, HttpOnly: true, Secure: GetEnv().isSecure, Expires: s.expirationTime, } } type SessionStore struct { l sync.RWMutex ctx context.Context fnCancel context.CancelFunc sessions map[string]*Session } func NewSessionStore(ctx context.Context) *SessionStore { ctxChild, fnCancel := context.WithCancel(ctx) s := &SessionStore{ ctx: ctxChild, fnCancel: fnCancel, sessions: map[string]*Session{}, } s.purgeWorker() return s } func (s *SessionStore) purge() { s.l.Lock() defer s.l.Unlock() now := time.Now() toDelete := []*Session{} for _, session := range s.sessions { if now.After(session.expirationTime) { toDelete = append(toDelete, session) } } for _, session := range toDelete { log.Debug().Str("sessionId", session.sessionID).Msg("purge expired session") delete(s.sessions, session.sessionID) } } func (s *SessionStore) purgeWorker() { ticker := time.NewTicker(10 * time.Second) //nolint go func() { for { select { case <-ticker.C: s.purge() case <-s.ctx.Done(): log.Info().Msg("purge worker stopped") ticker.Stop() return } } }() } func (s *SessionStore) Stop() { s.fnCancel() } func (s *SessionStore) Done() <-chan struct{} { return s.ctx.Done() } func (s *SessionStore) NewSession() (*Session, error) { sessionID, err := generateSessionID() if err != nil { log.Err(err).Msg("unable to generate sessionId") return nil, err } s.l.Lock() defer s.l.Unlock() if _, ok := s.sessions[sessionID]; ok { log.Error().Str("sessionId", sessionID).Msg("sessionId collision") return nil, ErrSessionIDCollision } now := time.Now().Add(GetEnv().GetSessionExpirationDuration()) session := Session{expirationTime: now, sessionID: sessionID} s.sessions[sessionID] = &session return &session, nil } func (s *SessionStore) IsLogged(r *http.Request) bool { cookie, err := r.Cookie("session_id") if err != nil { return false } s.l.RLock() defer s.l.RUnlock() _, ok := s.sessions[cookie.Value] return ok }