From b22151d4b7962249636fc5077777edcdc768c0e0 Mon Sep 17 00:00:00 2001 From: Gerasimos Maropoulos Date: Wed, 25 Apr 2018 05:29:19 +0300 Subject: [PATCH] Update to version 10.6.1 | Re-implement the BoltDB as built'n session database and more. Please read the HISTORY file for further details Former-commit-id: fa68a914bec5fe4f595bdeaea84ecab6374ba643 --- HISTORY.md | 6 + HISTORY_GR.md | 4 + HISTORY_ZH.md | 4 + README.md | 2 +- README_GR.md | 2 +- README_RU.md | 2 +- README_ZH.md | 2 +- VERSION | 2 +- _examples/sessions/database/badger/main.go | 2 +- _examples/sessions/database/boltdb/main.go | 96 ++++++ _examples/sessions/database/redis/main.go | 6 +- core/maintenance/version.go | 2 +- doc.go | 2 +- sessions/cookie.go | 6 +- sessions/sessiondb/badger/database.go | 31 +- sessions/sessiondb/boltdb/database.go | 362 +++++++++++++++++++++ 16 files changed, 507 insertions(+), 24 deletions(-) create mode 100644 _examples/sessions/database/boltdb/main.go create mode 100644 sessions/sessiondb/boltdb/database.go diff --git a/HISTORY.md b/HISTORY.md index 2702093d..da9091be 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -17,6 +17,12 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. +# We, 25 April 2018 | v10.6.1 + +- Re-implement the [BoltDB](https://github.com/coreos/bbolt) as built'n back-end storage for sessions(`sessiondb`) using the latest features: [/sessions/sessiondb/boltdb/database.go](sessions/sessiondb/boltdb/database.go), example can be found at [/_examples/sessions/database/boltdb/main.go](_examples/sessions/database/boltdb/main.go). +- Fix a minor issue on [Badger sessiondb example](_examples/sessions/database/badger/main.go). Its `sessions.Config { Expires }` field was `2 *time.Second`, it's `45 *time.Minute` now. +- Other minor improvements to the badger sessiondb. + # Su, 22 April 2018 | v10.6.0 - Fix open redirect by @wozz via PR: https://github.com/kataras/iris/pull/972. diff --git a/HISTORY_GR.md b/HISTORY_GR.md index f0bc1cc4..ac144fe9 100644 --- a/HISTORY_GR.md +++ b/HISTORY_GR.md @@ -17,6 +17,10 @@ **Πώς να αναβαθμίσετε**: Ανοίξτε την γραμμή εντολών σας και εκτελέστε αυτήν την εντολή: `go get -u github.com/kataras/iris` ή αφήστε το αυτόματο updater να το κάνει αυτό για σας. +# We, 25 April 2018 | v10.6.1 + +This history entry is not translated yet to the Greek language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#we-25-april-2018--v1061) instead. + # Su, 22 April 2018 | v10.6.0 This history entry is not translated yet to the Greek language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-22-april-2018--v1060) instead. diff --git a/HISTORY_ZH.md b/HISTORY_ZH.md index ade5aa42..71532217 100644 --- a/HISTORY_ZH.md +++ b/HISTORY_ZH.md @@ -37,6 +37,10 @@ > 请记住,如果您无法升级,那么就不要这样做,我们在此版本中没有任何安全修复程序,但在某些时候建议您最好进行升级,我们总是会添加您喜欢的新功能! +# We, 25 April 2018 | v10.6.1 + +This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#we-25-april-2018--v1061) instead. + # Su, 22 April 2018 | v10.6.0 This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-22-april-2018--v1060) instead. diff --git a/README.md b/README.md index df51639c..c99c0a7a 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ _Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ ## Support -- [HISTORY](HISTORY.md#sa-24-march-2018--v1050) file is your best friend, it contains information about the latest features and changes +- [HISTORY](HISTORY.md#we-25-april-2018--v1061) file is your best friend, it contains information about the latest features and changes - Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues) - Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com) - Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) diff --git a/README_GR.md b/README_GR.md index b586d16f..7a286e3f 100644 --- a/README_GR.md +++ b/README_GR.md @@ -108,7 +108,7 @@ _Η τελευταία ενημέρωση έγινε την [Τρίτη, 21 Νο ## Υποστήριξη -- To [HISTORY](HISTORY_GR.md#sa-24-march-2018--v1050) αρχείο είναι ο καλύτερος σας φίλος, περιέχει πληροφορίες σχετικά με τις τελευταίες λειτουργίες(features) και αλλαγές +- To [HISTORY](HISTORY_GR.md#we-25-april-2018--v1061) αρχείο είναι ο καλύτερος σας φίλος, περιέχει πληροφορίες σχετικά με τις τελευταίες λειτουργίες(features) και αλλαγές - Μήπως τυχαίνει να βρήκατε κάποιο bug; Δημοσιεύστε το στα [github issues](https://github.com/kataras/iris/issues) - Έχετε οποιεσδήποτε ερωτήσεις ή πρέπει να μιλήσετε με κάποιον έμπειρο για την επίλυση ενός προβλήματος σε πραγματικό χρόνο; Ελάτε μαζί μας στην [συνομιλία κοινότητας](https://chat.iris-go.com) - Συμπληρώστε την αναφορά εμπειρίας χρήστη κάνοντας κλικ [εδώ](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) diff --git a/README_RU.md b/README_RU.md index 0d9bac02..05c53bef 100644 --- a/README_RU.md +++ b/README_RU.md @@ -106,7 +106,7 @@ _Обновлено: [Вторник, 21 ноября 2017 г.](_benchmarks/READ ## Поддержка -- Файл [HISTORY](HISTORY.md#sa-24-march-2018--v1050) - ваш лучший друг, он содержит информацию о последних особенностях и всех изменениях +- Файл [HISTORY](HISTORY.md#we-25-april-2018--v1061) - ваш лучший друг, он содержит информацию о последних особенностях и всех изменениях - Вы случайно обнаружили ошибку? Опубликуйте ее на [Github вопросы](https://github.com/kataras/iris/issues) - У Вас есть какие-либо вопросы или Вам нужно поговорить с кем-то, кто бы смог решить Вашу проблему в режиме реального времени? Присоединяйтесь к нам в [чате сообщества](https://chat.iris-go.com) - Заполните наш отчет о пользовательском опыте на основе формы, нажав [здесь](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) diff --git a/README_ZH.md b/README_ZH.md index cd8e7f2c..6ee36718 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -102,7 +102,7 @@ _更新于: [2017年11月21日星期二](_benchmarks/README_UNIX.md)_ ## 支持 -- [更新记录](HISTORY_ZH.md#sa-24-march-2018--v1050) 是您最好的朋友,它包含有关最新功能和更改的信息 +- [更新记录](HISTORY_ZH.md#we-25-april-2018--v1061) 是您最好的朋友,它包含有关最新功能和更改的信息 - 你碰巧找到了一个错误? 请提交 [github issues](https://github.com/kataras/iris/issues) - 您是否有任何疑问或需要与有经验的人士交谈以实时解决问题? [加入我们的聊天](https://chat.iris-go.com) - [点击这里完成我们基于表单的用户体验报告](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) diff --git a/VERSION b/VERSION index c9c2715d..ef1e0c88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -10.6.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-22-april-2018--v1060 \ No newline at end of file +10.6.1:https://github.com/kataras/iris/blob/master/HISTORY.md#we-25-april-2018--v1061 \ No newline at end of file diff --git a/_examples/sessions/database/badger/main.go b/_examples/sessions/database/badger/main.go index 20bbc809..26e634b2 100644 --- a/_examples/sessions/database/badger/main.go +++ b/_examples/sessions/database/badger/main.go @@ -24,7 +24,7 @@ func main() { sess := sessions.New(sessions.Config{ Cookie: "sessionscookieid", - Expires: 45 * time.Second, // <=0 means unlimited life. Defaults to 0. + Expires: 45 * time.Minute, // <=0 means unlimited life. Defaults to 0. }) // diff --git a/_examples/sessions/database/boltdb/main.go b/_examples/sessions/database/boltdb/main.go new file mode 100644 index 00000000..87f241bd --- /dev/null +++ b/_examples/sessions/database/boltdb/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "os" + "time" + + "github.com/kataras/iris" + + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/sessions/sessiondb/boltdb" +) + +func main() { + db, err := boltdb.New("./sessions.db", os.FileMode(0750)) + if err != nil { + panic(err) + } + + // close and unlobkc the database when control+C/cmd+C pressed + iris.RegisterOnInterrupt(func() { + db.Close() + }) + + defer db.Close() // close and unlock the database if application errored. + + sess := sessions.New(sessions.Config{ + Cookie: "sessionscookieid", + Expires: 45 * time.Minute, // <=0 means unlimited life. Defaults to 0. + }) + + // + // IMPORTANT: + // + sess.UseDatabase(db) + + // the rest of the code stays the same. + app := iris.New() + + app.Get("/", func(ctx iris.Context) { + ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") + }) + app.Get("/set", func(ctx iris.Context) { + s := sess.Start(ctx) + //set session values + s.Set("name", "iris") + + //test if setted here + ctx.Writef("All ok session value of the 'name' is: %s", s.GetString("name")) + }) + + app.Get("/set/{key}/{value}", func(ctx iris.Context) { + key, value := ctx.Params().Get("key"), ctx.Params().Get("value") + s := sess.Start(ctx) + // set session values + s.Set(key, value) + + // test if setted here + ctx.Writef("All ok session value of the '%s' is: %s", key, s.GetString(key)) + }) + + app.Get("/get", func(ctx iris.Context) { + // get a specific key, as string, if no found returns just an empty string + name := sess.Start(ctx).GetString("name") + + ctx.Writef("The 'name' on the /set was: %s", name) + }) + + app.Get("/get/{key}", func(ctx iris.Context) { + // get a specific key, as string, if no found returns just an empty string + name := sess.Start(ctx).GetString(ctx.Params().Get("key")) + + ctx.Writef("The name on the /set was: %s", name) + }) + + app.Get("/delete", func(ctx iris.Context) { + // delete a specific key + sess.Start(ctx).Delete("name") + }) + + app.Get("/clear", func(ctx iris.Context) { + // removes all entries + sess.Start(ctx).Clear() + }) + + app.Get("/destroy", func(ctx iris.Context) { + //destroy, removes the entire session data and cookie + sess.Destroy(ctx) + }) + + app.Get("/update", func(ctx iris.Context) { + // updates expire date with a new date + sess.ShiftExpiration(ctx) + }) + + app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) +} diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index 4ed4be1a..0b9d2e0e 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -15,13 +15,13 @@ import ( func main() { // replace with your running redis' server settings: db := redis.New(service.Config{ - Network: service.DefaultRedisNetwork, - Addr: service.DefaultRedisAddr, + Network: "tcp", + Addr: "127.0.0.1:6379", Password: "", Database: "", MaxIdle: 0, MaxActive: 0, - IdleTimeout: service.DefaultRedisIdleTimeout, + IdleTimeout: time.Duration(5) * time.Minute, Prefix: ""}) // optionally configure the bridge between your redis server // close connection when control+C/cmd+C diff --git a/core/maintenance/version.go b/core/maintenance/version.go index 4bbafb20..b5041125 100644 --- a/core/maintenance/version.go +++ b/core/maintenance/version.go @@ -13,7 +13,7 @@ import ( const ( // Version is the string representation of the current local Iris Web Framework version. - Version = "10.6.0" + Version = "10.6.1" ) // CheckForUpdates checks for any available updates diff --git a/doc.go b/doc.go index 3fca59be..242b2c8c 100644 --- a/doc.go +++ b/doc.go @@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub: Current Version -10.6.0 +10.6.1 Installation diff --git a/sessions/cookie.go b/sessions/cookie.go index b23092ca..0d2363dd 100644 --- a/sessions/cookie.go +++ b/sessions/cookie.go @@ -1,6 +1,7 @@ package sessions import ( + "net" "net/http" "strconv" "strings" @@ -26,13 +27,10 @@ func GetCookie(ctx context.Context, name string) string { } return c.Value - - // return ctx.GetCookie(name) } // AddCookie adds a cookie func AddCookie(ctx context.Context, cookie *http.Cookie, reclaim bool) { - // http.SetCookie(ctx.ResponseWriter(), cookie) if reclaim { ctx.Request().AddCookie(cookie) } @@ -65,7 +63,7 @@ func RemoveCookie(ctx context.Context, config Config) { // IsValidCookieDomain returns true if the receiver is a valid domain to set // valid means that is recognised as 'domain' by the browser, so it(the cookie) can be shared with subdomains also func IsValidCookieDomain(domain string) bool { - if domain == "0.0.0.0" || domain == "127.0.0.1" { + if net.IP([]byte(domain)).IsLoopback() { // for these type of hosts, we can't allow subdomains persistence, // the web browser doesn't understand the mysubdomain.0.0.0.0 and mysubdomain.127.0.0.1 mysubdomain.32.196.56.181. as scorrectly ubdomains because of the many dots // so don't set a cookie domain here, let browser handle this diff --git a/sessions/sessiondb/badger/database.go b/sessions/sessiondb/badger/database.go index 9b0efeaf..339f4076 100644 --- a/sessions/sessiondb/badger/database.go +++ b/sessions/sessiondb/badger/database.go @@ -1,8 +1,10 @@ package badger import ( + "bytes" "os" "runtime" + "sync/atomic" "time" "github.com/kataras/golog" @@ -24,6 +26,8 @@ type Database struct { // it's initialized at `New` or `NewFromDB`. // Can be used to get stats. Service *badger.DB + + closed uint32 // if 1 is closed. } var _ sessions.Database = (*Database)(nil) @@ -76,7 +80,7 @@ func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime txn := db.Service.NewTransaction(true) defer txn.Commit(nil) - bsid := []byte(sid) + bsid := makePrefix(sid) item, err := txn.Get(bsid) if err == nil { // found, return the expiration. @@ -98,10 +102,14 @@ func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime return sessions.LifeTime{} // session manager will handle the rest. } -var delim = byte('*') +var delim = byte('_') + +func makePrefix(sid string) []byte { + return append([]byte(sid), delim) +} func makeKey(sid, key string) []byte { - return append([]byte(sid), append([]byte(key), delim)...) + return append(makePrefix(sid), []byte(key)...) } // Set sets a key value of a specific session. @@ -114,8 +122,8 @@ func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, valu } err = db.Service.Update(func(txn *badger.Txn) error { - return txn.SetWithTTL(makeKey(sid, key), valueBytes, lifetime.DurationUntilExpiration()) - // return txn.Set(makeKey(sid, key), valueBytes) + dur := lifetime.DurationUntilExpiration() + return txn.SetWithTTL(makeKey(sid, key), valueBytes, dur) }) if err != nil { @@ -149,7 +157,7 @@ func (db *Database) Get(sid string, key string) (value interface{}) { // Visit loops through all session keys and values. func (db *Database) Visit(sid string, cb func(key string, value interface{})) { - prefix := append([]byte(sid), delim) + prefix := makePrefix(sid) txn := db.Service.NewTransaction(false) defer txn.Discard() @@ -171,7 +179,7 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) { continue } - cb(string(item.Key()), value) + cb(string(bytes.TrimPrefix(item.Key(), prefix)), value) } } @@ -184,7 +192,7 @@ var iterOptionsNoValues = badger.IteratorOptions{ // Len returns the length of the session's entries (keys). func (db *Database) Len(sid string) (n int) { - prefix := append([]byte(sid), delim) + prefix := makePrefix(sid) txn := db.Service.NewTransaction(false) iter := txn.NewIterator(iterOptionsNoValues) @@ -211,7 +219,7 @@ func (db *Database) Delete(sid string, key string) (deleted bool) { // Clear removes all session key values but it keeps the session entry. func (db *Database) Clear(sid string) { - prefix := append([]byte(sid), delim) + prefix := makePrefix(sid) txn := db.Service.NewTransaction(true) defer txn.Commit(nil) @@ -241,9 +249,14 @@ func (db *Database) Close() error { } func closeDB(db *Database) error { + if atomic.LoadUint32(&db.closed) > 0 { + return nil + } err := db.Service.Close() if err != nil { golog.Warnf("closing the badger connection: %v", err) + } else { + atomic.StoreUint32(&db.closed, 1) } return err } diff --git a/sessions/sessiondb/boltdb/database.go b/sessions/sessiondb/boltdb/database.go new file mode 100644 index 00000000..23edc0b2 --- /dev/null +++ b/sessions/sessiondb/boltdb/database.go @@ -0,0 +1,362 @@ +package boltdb + +import ( + "os" + "path/filepath" + "runtime" + "time" + + "github.com/coreos/bbolt" + "github.com/kataras/golog" + "github.com/kataras/iris/core/errors" + "github.com/kataras/iris/sessions" +) + +// DefaultFileMode used as the default database's "fileMode" +// for creating the sessions directory path, opening and write +// the session boltdb(file-based) storage. +var ( + DefaultFileMode = 0755 +) + +// Database the BoltDB(file-based) session storage. +type Database struct { + table []byte + // Service is the underline BoltDB database connection, + // it's initialized at `New` or `NewFromDB`. + // Can be used to get stats. + Service *bolt.DB +} + +var errPathMissing = errors.New("path is required") + +// New creates and returns a new BoltDB(file-based) storage +// instance based on the "path". +// Path should include the filename and the directory(aka fullpath), i.e sessions/store.db. +// +// It will remove any old session files. +func New(path string, fileMode os.FileMode) (*Database, error) { + if path == "" { + golog.Error(errPathMissing) + return nil, errPathMissing + } + + if fileMode <= 0 { + fileMode = os.FileMode(DefaultFileMode) + } + + // create directories if necessary + if err := os.MkdirAll(filepath.Dir(path), fileMode); err != nil { + golog.Errorf("error while trying to create the necessary directories for %s: %v", path, err) + return nil, err + } + + service, err := bolt.Open(path, fileMode, + &bolt.Options{Timeout: 20 * time.Second}, + ) + + if err != nil { + golog.Errorf("unable to initialize the BoltDB-based session database: %v", err) + return nil, err + } + + return NewFromDB(service, "sessions") +} + +// NewFromDB same as `New` but accepts an already-created custom boltdb connection instead. +func NewFromDB(service *bolt.DB, bucketName string) (*Database, error) { + bucket := []byte(bucketName) + + service.Update(func(tx *bolt.Tx) (err error) { + _, err = tx.CreateBucketIfNotExists(bucket) + return + }) + + db := &Database{table: bucket, Service: service} + + runtime.SetFinalizer(db, closeDB) + return db, db.cleanup() +} + +func (db *Database) getBucket(tx *bolt.Tx) *bolt.Bucket { + return tx.Bucket(db.table) +} + +func (db *Database) getBucketForSession(tx *bolt.Tx, sid string) *bolt.Bucket { + b := db.getBucket(tx).Bucket([]byte(sid)) + if b == nil { + // session does not exist, it shouldn't happen, session bucket creation happens once at `Acquire`, + // no need to accept the `bolt.bucket.CreateBucketIfNotExists`'s performance cost. + golog.Debugf("unreachable session access for '%s'", sid) + } + + return b +} + +var ( + expirationBucketName = []byte("expiration") + delim = []byte("_") +) + +// expiration lives on its own bucket for each session bucket. +func getExpirationBucketName(bsid []byte) []byte { + return append(bsid, append(delim, expirationBucketName...)...) +} + +// Cleanup removes any invalid(have expired) session entries on initialization. +func (db *Database) cleanup() error { + return db.Service.Update(func(tx *bolt.Tx) error { + b := db.getBucket(tx) + c := b.Cursor() + // loop through all buckets, find one with expiration. + for bsid, v := c.First(); bsid != nil; bsid, v = c.Next() { + if len(bsid) == 0 { // empty key, continue to the next session bucket. + continue + } + + expirationName := getExpirationBucketName(bsid) + if bExp := b.Bucket(expirationName); bExp != nil { // has expiration. + _, expValue := bExp.Cursor().First() // the expiration bucket contains only one key(we don't care, see `Acquire`) value(time.Time) pair. + if expValue == nil { + golog.Debugf("cleanup: expiration is there but its value is empty '%s'", v) // should never happen. + continue + } + + var expirationTime time.Time + if err := sessions.DefaultTranscoder.Unmarshal(expValue, &expirationTime); err != nil { + golog.Debugf("cleanup: unable to retrieve expiration value for '%s'", v) + continue + } + + if expirationTime.Before(time.Now()) { + // expired, delete the expiration bucket. + if err := b.DeleteBucket(expirationName); err != nil { + golog.Debugf("cleanup: unable to destroy a session '%s'", bsid) + return err + } + + // and the session bucket, if any. + return b.DeleteBucket(bsid) + } + } + } + + return nil + }) +} + +var expirationKey = []byte("exp") // it can be random. + +// Acquire receives a session's lifetime from the database, +// if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. +func (db *Database) Acquire(sid string, expires time.Duration) (lifetime sessions.LifeTime) { + bsid := []byte(sid) + err := db.Service.Update(func(tx *bolt.Tx) (err error) { + root := db.getBucket(tx) + + if expires > 0 { // should check or create the expiration bucket. + name := getExpirationBucketName(bsid) + b := root.Bucket(name) + if b == nil { + // not found, create a session bucket and an expiration bucket and save the given "expires" of time.Time, + // don't return a lifetime, let it empty, session manager will do its job. + b, err = root.CreateBucket(name) + if err != nil { + golog.Debugf("unable to create a session bucket for '%s': %v", sid, err) + return err + } + + expirationTime := time.Now().Add(expires) + timeBytes, err := sessions.DefaultTranscoder.Marshal(expirationTime) + if err != nil { + golog.Debugf("unable to set an expiration value on session expiration bucket for '%s': %v", sid, err) + return err + } + + if err := b.Put(expirationKey, timeBytes); err == nil { + // create the session bucket now, so the rest of the calls can be easly get the bucket without any further checks. + _, err = root.CreateBucket(bsid) + } + + return err + } + + // found, get the associated expiration bucket, wrap its value and return. + _, expValue := b.Cursor().First() + if expValue == nil { + return nil // does not expire. + } + + var expirationTime time.Time + if err = sessions.DefaultTranscoder.Unmarshal(expValue, &expirationTime); err != nil { + golog.Debugf("acquire: unable to retrieve expiration value for '%s', value was: '%s': %v", sid, expValue, err) + return + } + + lifetime = sessions.LifeTime{Time: expirationTime} + return nil + } + + // does not expire, just create the session bucket if not exists so we can be ready later on. + _, err = root.CreateBucketIfNotExists(bsid) + return + }) + + if err != nil { + golog.Debugf("unable to acquire session '%s': %v", sid, err) + return sessions.LifeTime{} + } + + return +} + +func makeKey(key string) []byte { + return []byte(key) +} + +// Set sets a key value of a specific session. +// Ignore the "immutable". +func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) { + valueBytes, err := sessions.DefaultTranscoder.Marshal(value) + if err != nil { + golog.Debug(err) + return + } + + err = db.Service.Update(func(tx *bolt.Tx) error { + b := db.getBucketForSession(tx, sid) + if b == nil { + return nil + } + + // Author's notes: + // expiration is handlded by the session manager for the whole session, so the `db.Destroy` will be called when and if needed. + // Therefore we don't have to implement a TTL here, but we need a `db.Cleanup`, as we did previously, method to delete any expired if server restarted + // (badger does not need a `Cleanup` because we set the TTL based on the lifetime.DurationUntilExpiration()). + return b.Put(makeKey(key), valueBytes) + }) + + if err != nil { + golog.Debug(err) + } +} + +// Get retrieves a session value based on the key. +func (db *Database) Get(sid string, key string) (value interface{}) { + err := db.Service.View(func(tx *bolt.Tx) error { + b := db.getBucketForSession(tx, sid) + if b == nil { + return nil + } + + valueBytes := b.Get(makeKey(key)) + if len(valueBytes) == 0 { + return nil + } + + return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) + }) + + if err != nil { + golog.Debugf("session '%s' key '%s' not found", sid, key) + } + + return +} + +// Visit loops through all session keys and values. +func (db *Database) Visit(sid string, cb func(key string, value interface{})) { + db.Service.View(func(tx *bolt.Tx) error { + b := db.getBucketForSession(tx, sid) + if b == nil { + return nil + } + + return b.ForEach(func(k []byte, v []byte) error { + var value interface{} + if err := sessions.DefaultTranscoder.Unmarshal(v, &value); err != nil { + golog.Debugf("unable to retrieve value of key '%s' of '%s': %v", k, sid, err) + return err + } + + cb(string(k), value) + return nil + }) + }) +} + +// Len returns the length of the session's entries (keys). +func (db *Database) Len(sid string) (n int) { + db.Service.View(func(tx *bolt.Tx) error { + b := db.getBucketForSession(tx, sid) + if b == nil { + return nil + } + + n = b.Stats().KeyN + return nil + }) + + return +} + +var errNotFound = errors.New("not found") + +// Delete removes a session key value based on its key. +func (db *Database) Delete(sid string, key string) (deleted bool) { + err := db.Service.Update(func(tx *bolt.Tx) error { + b := db.getBucketForSession(tx, sid) + if b == nil { + return errNotFound + } + + return b.Delete(makeKey(key)) + }) + + return err == nil +} + +// Clear removes all session key values but it keeps the session entry. +func (db *Database) Clear(sid string) { + db.Service.Update(func(tx *bolt.Tx) error { + b := db.getBucketForSession(tx, sid) + if b == nil { + return nil + } + + return b.ForEach(func(k []byte, v []byte) error { + return b.Delete(k) + }) + }) +} + +// Release destroys the session, it clears and removes the session entry, +// session manager will create a new session ID on the next request after this call. +func (db *Database) Release(sid string) { + db.Service.Update(func(tx *bolt.Tx) error { + // delete the session bucket. + b := db.getBucket(tx) + bsid := []byte(sid) + if err := b.DeleteBucket(bsid); err != nil { + return err + } + + // and try to delete the associated expiration bucket, if exists, ignore error. + b.DeleteBucket(getExpirationBucketName(bsid)) + return nil + }) +} + +// Close shutdowns the BoltDB connection. +func (db *Database) Close() error { + return closeDB(db) +} + +func closeDB(db *Database) error { + err := db.Service.Close() + if err != nil { + golog.Warnf("closing the BoltDB connection: %v", err) + } + + return err +}