package sessions import ( "sync" "time" "github.com/kataras/iris/core/memstore" ) type ( // provider contains the sessions and external databases (load and update). // It's the session memory manager provider struct { // we don't use RWMutex because all actions have read and write at the same action function. // (or write to a *Session's value which is race if we don't lock) // narrow locks are fasters but are useless here. mu sync.Mutex sessions map[string]*Session databases []Database } ) // newProvider returns a new sessions provider func newProvider() *provider { return &provider{ sessions: make(map[string]*Session, 0), databases: make([]Database, 0), } } // RegisterDatabase adds a session database // a session db doesn't have write access func (p *provider) RegisterDatabase(db Database) { p.mu.Lock() // for any case p.databases = append(p.databases, db) p.mu.Unlock() } // newSession returns a new session from sessionid func (p *provider) newSession(sid string, expires time.Duration) *Session { onExpire := func() { p.Destroy(sid) } values, lifetime := p.loadSessionFromDB(sid) // simple and straight: if !lifetime.IsZero() { // if stored time is not zero // start a timer based on the stored time, if not expired. lifetime.Revive(onExpire) } else { // Remember: if db not exist or it has been expired // then the stored time will be zero(see loadSessionFromDB) and the values will be empty. // // Even if the database has an unlimited session (possible by a previous app run) // priority to the "expires" is given, // again if <=0 then it does nothing. lifetime.Begin(expires, onExpire) } // I ended up without the need of a duration field on lifetime, // but these are some of my previous comments, they will be required for any future change. // // OK I think go has a bug when gob on embedded time.Time // even if we `gob.Register(LifeTime)` // the OriginalDuration is not saved to the gob file and it cannot be retrieved, it's always 0. // But if we do not embed the `time.Time` inside the `LifeTime` then // it's working. // i.e type LifeTime struct { time.Time; OriginalDuration time.Duration} -> this doesn't // type LifeTime struct {Time time.Time; OriginalDuration time.Duration} -> this works // So we have two options: // 1. don't embed the time.Time -> we will have to use lifetime.Time to get its functions, which doesn't seems right to me // 2. embed the time.Time and compare their times with `lifetime.After(time.Now().Add(expires))`, it seems right but it // should be slower. // // I'll use the 1. and put some common time.Time functions, like After, IsZero on the `LifeTime` type too. // // if db exists but its lifetime is bigger than the expires (very raire, // the source code should be compatible with the databases, // should we print a warning to the user? it is his/her fault // use the database's lifetime or the configurated? // if values.Len() > 0 && lifetime.OriginalDuration != expires { // golog.Warnf(`session database: stored expire time(dur=%d) is differnet than the configuration(dur=%d) // application will use the configurated one`, lifetime.OriginalDuration, expires) // lifetime.Reset(expires) // } sess := &Session{ sid: sid, provider: p, values: values, flashes: make(map[string]*flashMessage), lifetime: lifetime, } return sess } func (p *provider) loadSessionFromDB(sid string) (memstore.Store, LifeTime) { var store memstore.Store var lifetime LifeTime firstValidIdx := 1 for i, n := 0, len(p.databases); i < n; i++ { storeDB := p.databases[i].Load(sid) if storeDB.Lifetime.HasExpired() { // if expired then skip this db firstValidIdx++ continue } if lifetime.IsZero() { // update the lifetime to the most valid lifetime = storeDB.Lifetime } if n == firstValidIdx { // if one database then set the store as it is store = storeDB.Values } else { // else append this database's key-value pairs // to the store storeDB.Values.Visit(func(key string, value interface{}) { store.Set(key, value) }) } } // Note: if one database and it's being expired then the lifetime will be zero(unlimited) // this by itself is wrong but on the `newSession` we make check of this case too and update the lifetime // if the configuration has expiration registered. /// TODO: bug on destroy doesn't being remove the file // we will have to see it, it's not db's problem it's here on provider destroy or lifetime onExpire. return store, lifetime } // Init creates the session and returns it func (p *provider) Init(sid string, expires time.Duration) *Session { newSession := p.newSession(sid, expires) p.mu.Lock() p.sessions[sid] = newSession p.mu.Unlock() return newSession } // UpdateExpiration update expire date of a session. // if expires > 0 then it updates the destroy task. // if expires <=0 then it does nothing, to destroy a session call the `Destroy` func instead. func (p *provider) UpdateExpiration(sid string, expires time.Duration) bool { if expires <= 0 { return false } p.mu.Lock() sess, found := p.sessions[sid] p.mu.Unlock() if !found { return false } sess.lifetime.Shift(expires) return true } // Read returns the store which sid parameter belongs func (p *provider) Read(sid string, expires time.Duration) *Session { p.mu.Lock() if sess, found := p.sessions[sid]; found { sess.runFlashGC() // run the flash messages GC, new request here of existing session p.mu.Unlock() return sess } p.mu.Unlock() return p.Init(sid, expires) // if not found create new } // Destroy destroys the session, removes all sessions and flash values, // the session itself and updates the registered session databases, // this called from sessionManager which removes the client's cookie also. func (p *provider) Destroy(sid string) { p.mu.Lock() if sess, found := p.sessions[sid]; found { p.deleteSession(sess) } p.mu.Unlock() } // DestroyAll removes all sessions // from the server-side memory (and database if registered). // Client's session cookie will still exist but it will be reseted on the next request. func (p *provider) DestroyAll() { p.mu.Lock() for _, sess := range p.sessions { p.deleteSession(sess) } p.mu.Unlock() } func (p *provider) deleteSession(sess *Session) { delete(p.sessions, sess.sid) syncDatabases(p.databases, acquireSyncPayload(sess, ActionDestroy)) }