diff --git a/session.go b/session.go index 36fb586..93481d9 100644 --- a/session.go +++ b/session.go @@ -6,99 +6,134 @@ import ( "encoding/base64" "encoding/json" "io" + "maps" "net/http" - "net/url" "sync" "time" "github.com/redis/go-redis/v9" ) -var RDb = redis.NewClient(&redis.Options{ - Addr: "localhost:6379", - Password: "123", - DB: 0, // use default DB -}) - -var expires = int(96 * time.Hour) -var SessionManager = InitSession("sid", expires) +//var RDb = redis.NewClient(&redis.Options{ +// Addr: "localhost:6379", +// Password: "123", +// DB: 0, // use default DB +//}) +// var expires = time.Duration(96 * time.Hour) type SessionInterface interface { - Write() (error, bool) + redisRead() error + redisWrite() error UpdateTTL() error - Read() (error, bool) - Set(key string, value interface{}) - Get(key string) interface{} + Set(key string, value any) error + Get(key string) any ID() string - Delete(string) + Delete(string) error Destroy() error } type Manager struct { + redisDB *redis.Client privateCookieName string - lock sync.Mutex maxLifeTTL time.Duration -} - -func InitSession(cookieName string, ttl int) *Manager { - return &Manager{ - privateCookieName: cookieName, - lock: sync.Mutex{}, - maxLifeTTL: time.Duration(ttl), - } + secure bool + cache map[string]SessionInterface } type Session struct { sid string - Storage *SessionStorage + storage *sessionStorage ttl time.Duration + manager *Manager + mutex sync.RWMutex } -type SessionStorage struct { - Values map[string]interface{} +type sessionStorage struct { + Values map[string]any } -func (m *Manager) createSession() (*Session, *http.Cookie) { - v := make(map[string]interface{}, 0) +func NewManager(client *redis.Client, cookieName string, ttl time.Duration, secure bool) *Manager { + return &Manager{ + redisDB: client, + privateCookieName: cookieName, + maxLifeTTL: ttl, + secure: secure, + } +} + +func (m *Manager) createSession() (SessionInterface, *http.Cookie, error) { + v := make(map[string]any) session := &Session{ sid: m.sessionID(), - Storage: &SessionStorage{Values: v}, + storage: &sessionStorage{Values: v}, ttl: m.maxLifeTTL, + manager: m, } - session.Write() + err := session.redisWrite() + if err != nil { + return nil, nil, err + } + return session, m.newCookie(session.sid), nil +} - cookie := &http.Cookie{ +func (m *Manager) newCookie(sid string) *http.Cookie { + return &http.Cookie{ Name: m.privateCookieName, - Value: url.QueryEscape(session.sid), + Value: sid, Path: "/", HttpOnly: true, + Secure: m.secure, MaxAge: int(m.maxLifeTTL / time.Second), + SameSite: http.SameSiteStrictMode, } - - return session, cookie } func (m *Manager) GetOrCreateSession(w http.ResponseWriter, r *http.Request) (SessionInterface, error) { - m.lock.Lock() - defer m.lock.Unlock() - cookie, err := r.Cookie(m.privateCookieName) if err != nil || cookie.Value == "" { - session, cookie := m.createSession() - http.SetCookie(w, cookie) - return session, nil - } else { - sid, _ := url.QueryUnescape(cookie.Value) - session := &Session{sid: sid} - if _, ok := session.Read(); ok { - return session, nil - } else { - session, cookie := m.createSession() - http.SetCookie(w, cookie) - return session, nil + session, cookie, err := m.createSession() + if err != nil { + return nil, err } - + http.SetCookie(w, cookie) + m.cache[session.ID()] = session + return session, nil } + sid := cookie.Value + if cached, ok := m.cache[sid]; ok { + // refresh cache + if err = cached.redisRead(); err != nil { return nil, err } + if err = cached.UpdateTTL(); err != nil { return nil, err } + http.SetCookie(w, m.newCookie(cached.ID())) + return cached, nil + } + session := &Session{sid: sid, manager: m} + err = session.redisRead() + if err == nil { + // refresh cookie on read + http.SetCookie(w, m.newCookie(session.sid)) + if err := session.UpdateTTL(); err != nil { + newSession, cookie, err := m.createSession() + if err != nil { + return nil, err + } + http.SetCookie(w, cookie) + m.cache[newSession.ID()] = newSession + return newSession, nil + } + m.cache[session.ID()] = session + return session, nil + } + if err != redis.Nil { + return nil, err + } + newSession, cookie, err := m.createSession() + if err != nil { + return nil, err + } + http.SetCookie(w, cookie) + m.cache[newSession.ID()] = newSession + return newSession, nil } func (m *Manager) sessionID() string { @@ -110,70 +145,107 @@ func (m *Manager) sessionID() string { return base64.URLEncoding.EncodeToString(b) } -func (sm *Session) Read() (error, bool) { - val, err := RDb.Get(context.Background(), sm.sid).Result() +func (sm *Session) redisRead() error { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + val, err := sm.manager.redisDB.Get(ctx, "session:"+sm.sid).Result() if err == redis.Nil { - return err, false + cancel() + return err } + cancel() - st := &SessionStorage{} + st := &sessionStorage{} err = json.Unmarshal([]byte(val), st) if err != nil { - return err, false + return err } + sm.mutex.Lock() + sm.storage = st + sm.mutex.Unlock() - sm.Storage = st - - ttl, err := RDb.TTL(context.Background(), sm.sid).Result() + ctx, cancel = context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + // session ttl mirrors redis ttl + ttl, err := sm.manager.redisDB.TTL(ctx, "session:"+sm.sid).Result() if err != nil { - return err, false + return err } - + sm.mutex.Lock() sm.ttl = ttl - - return nil, true + sm.mutex.Unlock() + return nil } -func (sm *Session) Write() (error, bool) { - val, err := json.Marshal(sm.Storage) - if err != nil { - return err, false - } +func (sm *Session) redisWrite() error { + shallow := make(map[string]any, len(sm.storage.Values)) + maps.Copy(shallow, sm.storage.Values) - err = RDb.Set(context.Background(), sm.sid, val, sm.ttl).Err() + payload := &sessionStorage{Values: shallow} + val, err := json.Marshal(payload) if err != nil { - return err, false + return err } - - return nil, true + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + // on successful redisWrite ttl resets to max time + err = sm.manager.redisDB.Set(ctx, "session:"+sm.sid, val, sm.manager.maxLifeTTL).Err() + return err } func (sm *Session) UpdateTTL() error { - return RDb.Expire(context.Background(), sm.sid, time.Duration(expires)).Err() -} - -func (sm *Session) Get(key string) interface{} { - if val, ok := sm.Storage.Values[key]; ok { - return val - } else { - return nil + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + ok, err := sm.manager.redisDB.Expire(ctx, "session:"+sm.sid, sm.manager.maxLifeTTL).Result() + if err != nil { + return err } + if !ok { + return redis.Nil + } + return nil } func (sm *Session) ID() string { return sm.sid } -func (sm *Session) Set(key string, value interface{}) { - sm.Storage.Values[key] = value +func (sm *Session) Get(key string) any { + sm.mutex.RLock() + defer sm.mutex.RUnlock() + + if val, ok := sm.storage.Values[key]; ok { + return val + } else { + return nil + } } -func (sm *Session) Delete(key string) { - delete(sm.Storage.Values, key) +func (sm *Session) Set(key string, value any) error { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + sm.storage.Values[key] = value + return sm.redisWrite() +} + +func (sm *Session) Delete(key string) error { + sm.mutex.Lock() + defer sm.mutex.Unlock() + + delete(sm.storage.Values, key) + return sm.redisWrite() } func (sm *Session) Destroy() error { - sm.sid = "" - sm.Storage = &SessionStorage{} - return RDb.Del(context.Background(), sm.sid).Err() + sm.mutex.Lock() + defer sm.mutex.Unlock() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + err := sm.manager.redisDB.Del(ctx, "session:"+sm.sid).Err() + if err == nil { + sm.sid = "" + sm.storage = &sessionStorage{Values: map[string]any{}} + } + return err }