2017-02-15 19:06:19 +01:00
|
|
|
package sessions
|
|
|
|
|
|
|
|
import (
|
|
|
|
"sync"
|
|
|
|
"time"
|
2017-06-08 02:39:15 +02:00
|
|
|
|
|
|
|
"github.com/kataras/iris/core/memstore"
|
2017-02-15 19:06:19 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
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.
|
2017-07-10 17:32:42 +02:00
|
|
|
// (or write to a *Session's value which is race if we don't lock)
|
2017-02-15 19:06:19 +01:00
|
|
|
// narrow locks are fasters but are useless here.
|
|
|
|
mu sync.Mutex
|
2017-07-10 17:32:42 +02:00
|
|
|
sessions map[string]*Session
|
2017-02-15 19:06:19 +01:00
|
|
|
databases []Database
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
// newProvider returns a new sessions provider
|
|
|
|
func newProvider() *provider {
|
|
|
|
return &provider{
|
2017-07-10 17:32:42 +02:00
|
|
|
sessions: make(map[string]*Session, 0),
|
2017-02-15 19:06:19 +01:00
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
// newSession returns a new session from sessionid
|
|
|
|
func (p *provider) newSession(sid string, expires time.Duration) *Session {
|
|
|
|
onExpire := func() {
|
|
|
|
p.Destroy(sid)
|
2017-07-31 20:49:30 +02:00
|
|
|
}
|
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
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)
|
|
|
|
}
|
2017-07-31 20:49:30 +02:00
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
// 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)
|
|
|
|
// }
|
2017-07-31 20:49:30 +02:00
|
|
|
|
2017-07-10 17:32:42 +02:00
|
|
|
sess := &Session{
|
2017-02-15 19:06:19 +01:00
|
|
|
sid: sid,
|
|
|
|
provider: p,
|
2017-07-31 20:49:30 +02:00
|
|
|
values: values,
|
2017-02-15 19:06:19 +01:00
|
|
|
flashes: make(map[string]*flashMessage),
|
2017-08-07 05:04:35 +02:00
|
|
|
lifetime: lifetime,
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return sess
|
|
|
|
}
|
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
func (p *provider) loadSessionFromDB(sid string) (memstore.Store, LifeTime) {
|
2017-06-08 02:39:15 +02:00
|
|
|
var store memstore.Store
|
2017-08-07 05:04:35 +02:00
|
|
|
var lifetime LifeTime
|
2017-02-15 19:06:19 +01:00
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
firstValidIdx := 1
|
2017-02-15 19:06:19 +01:00
|
|
|
for i, n := 0, len(p.databases); i < n; i++ {
|
2017-08-07 05:04:35 +02:00
|
|
|
storeDB := p.databases[i].Load(sid)
|
|
|
|
if storeDB.Lifetime.HasExpired() { // if expired then skip this db
|
|
|
|
firstValidIdx++
|
|
|
|
continue
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|
2017-07-31 20:49:30 +02:00
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
if lifetime.IsZero() {
|
|
|
|
// update the lifetime to the most valid
|
|
|
|
lifetime = storeDB.Lifetime
|
2017-07-31 20:49:30 +02:00
|
|
|
}
|
2017-02-15 19:06:19 +01:00
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
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)
|
|
|
|
})
|
|
|
|
}
|
2017-07-31 20:49:30 +02:00
|
|
|
}
|
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
// 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.
|
2017-06-08 02:39:15 +02:00
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
/// 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
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Init creates the session and returns it
|
2017-07-10 17:32:42 +02:00
|
|
|
func (p *provider) Init(sid string, expires time.Duration) *Session {
|
2017-02-15 19:06:19 +01:00
|
|
|
newSession := p.newSession(sid, expires)
|
|
|
|
p.mu.Lock()
|
|
|
|
p.sessions[sid] = newSession
|
|
|
|
p.mu.Unlock()
|
|
|
|
return newSession
|
|
|
|
}
|
|
|
|
|
2017-08-14 15:21:51 +02:00
|
|
|
// UpdateExpiration update expire date of a session.
|
2017-08-07 05:04:35 +02:00
|
|
|
// 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.
|
2017-08-14 15:21:51 +02:00
|
|
|
func (p *provider) UpdateExpiration(sid string, expires time.Duration) bool {
|
2017-07-31 20:49:30 +02:00
|
|
|
if expires <= 0 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
p.mu.Lock()
|
|
|
|
sess, found := p.sessions[sid]
|
|
|
|
p.mu.Unlock()
|
|
|
|
if !found {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
sess.lifetime.Shift(expires)
|
2017-07-31 20:49:30 +02:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2017-02-15 19:06:19 +01:00
|
|
|
// Read returns the store which sid parameter belongs
|
2017-07-10 17:32:42 +02:00
|
|
|
func (p *provider) Read(sid string, expires time.Duration) *Session {
|
2017-02-15 19:06:19 +01:00
|
|
|
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()
|
2017-07-31 20:49:30 +02:00
|
|
|
|
2017-02-15 19:06:19 +01:00
|
|
|
return sess
|
|
|
|
}
|
|
|
|
p.mu.Unlock()
|
|
|
|
|
2017-07-31 20:49:30 +02:00
|
|
|
return p.Init(sid, expires) // if not found create new
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2017-08-07 05:04:35 +02:00
|
|
|
p.deleteSession(sess)
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|
|
|
|
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 {
|
2017-08-07 05:04:35 +02:00
|
|
|
p.deleteSession(sess)
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|
|
|
|
p.mu.Unlock()
|
2017-08-07 05:04:35 +02:00
|
|
|
}
|
2017-02-15 19:06:19 +01:00
|
|
|
|
2017-08-07 05:04:35 +02:00
|
|
|
func (p *provider) deleteSession(sess *Session) {
|
|
|
|
delete(p.sessions, sess.sid)
|
|
|
|
syncDatabases(p.databases, acquireSyncPayload(sess, ActionDestroy))
|
2017-02-15 19:06:19 +01:00
|
|
|
}
|