diff --git a/HISTORY.md b/HISTORY.md index 9e4949e5..c2206108 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Redis Driver is now based on the [go-redis](https://github.com/go-redis/redis/) module. Radix and redigo removed entirely. Sessions are now stored in hashes which fixes [issue #1610](https://github.com/kataras/iris/issues/1610). The only breaking change on default configuration is that the `redis.Config.Delim` option was removed. The redis sessions database driver is now defaults to the `&redis.GoRedisDriver{}`. End-developers can implement their own implementations too. The `Database#Close` is now automatically called on interrupt signals, no need to register it by yourself. + - Add builtin support for **[i18n pluralization](https://github.com/kataras/iris/tree/master/_examples/i18n/plurals)**. Please check out the [following yaml locale example](https://github.com/kataras/iris/tree/master/_examples/i18n/plurals/locales/en-US/welcome.yml) to see an overview of the supported formats. - Fix [#1650](https://github.com/kataras/iris/issues/1650) - Fix [#1649](https://github.com/kataras/iris/issues/1649) diff --git a/NOTICE b/NOTICE index 40869bc1..5f0c9def 100644 --- a/NOTICE +++ b/NOTICE @@ -4,9 +4,9 @@ ================================================================================ -The following 3rd-party software packages may be used by or distributed with iris. This document was automatically generated by FOSSA on 2020-5-8; any information relevant to third-party vendors listed below are collected using common, reasonable means. +The following 3rd-party software packages may be used by or distributed with iris. This document was automatically generated by FOSSA on 4 Oct 2020; any information relevant to third-party vendors listed below are collected using common, reasonable means. -Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c +Revision ID: 5fc50a00491616d5cd0cbce3abd8b699838e25ca ----------------- ----------------- ------------------------------------------ Package Version Website @@ -82,16 +82,13 @@ Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c b63c98b3c0 protobuf 6c66de79d66478d https://github.com/golang/protobuf 166c7ea05f5d2cc - af016fbd6b - radix 66d2cdb891459f2 https://github.com/mediocregopher/radix - 74a799f06760efa - 6f8f159a8c + af016fbd6b raymond b565731e1464263 https://github.com/aymerick/raymond de0bda75f2e45d9 7b54b60110 - redigo 37c69a26f6a2fb5 https://github.com/gomodule/redigo - 6c2caa506b29280 - 2203a90042 + go-redis 7125bf611e5d7d9 https://github.com/go-redis/redis + bb4487dd6cb80d8 + 88bad92d23 schema 1f5dc3fa1ac5179 https://github.com/iris-contrib/schema 78c014cb1df9954 0fa5b17f7e diff --git a/_examples/sessions/database/redis/main.go b/_examples/sessions/database/redis/main.go index f8333dc7..898dc5c9 100644 --- a/_examples/sessions/database/redis/main.go +++ b/_examples/sessions/database/redis/main.go @@ -6,8 +6,6 @@ import ( "strings" "time" - "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions/sessiondb/redis" @@ -24,24 +22,18 @@ func main() { Addr: getenv("REDIS_ADDR", "127.0.0.1:6379"), Timeout: time.Duration(30) * time.Second, MaxActive: 10, + Username: "", Password: "", Database: "", Prefix: "", - Delim: "-", - Driver: redis.Redigo(), // redis.Radix() can be used instead. + Driver: redis.GoRedis(), // defautls. }) // Optionally configure the underline driver: - // driver := redis.Redigo() - // driver.MaxIdle = ... - // driver.IdleTimeout = ... - // driver.Wait = ... - // redis.Config {Driver: driver} - - // Close connection when control+C/cmd+C - iris.RegisterOnInterrupt(func() { - db.Close() - }) + // driver := redis.GoRedis() + // driver.ClientOptions = redis.Options{...} + // driver.ClusterOptions = redis.ClusterOptions{...} + // redis.New(redis.Config{Driver: driver, ...}) defer db.Close() // close the database connection if application errored. diff --git a/_examples/sessions/overview/example/example.go b/_examples/sessions/overview/example/example.go index 64509b51..ed4fb5e4 100644 --- a/_examples/sessions/overview/example/example.go +++ b/_examples/sessions/overview/example/example.go @@ -57,12 +57,17 @@ func NewApp(sess *sessions.Sessions) *iris.Application { session := sessions.Get(ctx) session.Set("struct", BusinessModel{Name: "John Doe"}) - ctx.Writef("All ok session value of the 'struct' is: %v", session.Get("struct")) + ctx.WriteString("All ok session value of the 'struct' was set.") }) app.Get("/get-struct", func(ctx iris.Context) { session := sessions.Get(ctx) - ctx.Writef("Session value of the 'struct' is: %v", session.Get("struct")) + var v BusinessModel + if err := session.Decode("struct", &v); err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } + ctx.Writef("Session value of the 'struct' is: %#+v", v) }) app.Get("/set/{key}/{value}", func(ctx iris.Context) { @@ -158,7 +163,12 @@ func NewApp(sess *sessions.Sessions) *iris.Application { business := []BusinessModel{{Name: "Edward"}, {Name: "value 2"}} session.SetImmutable("businessEdit", business) - businessGet := session.Get("businessEdit").([]BusinessModel) + var businessGet []BusinessModel + err := session.Decode("businessEdit", &businessGet) + if err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } // try to change it, if we used `Set` instead of `SetImmutable` this // change will affect the underline array of the session's value "businessEdit", but now it will not. @@ -166,13 +176,19 @@ func NewApp(sess *sessions.Sessions) *iris.Application { }) app.Get("/get-immutable", func(ctx iris.Context) { - valSlice := sessions.Get(ctx).Get("businessEdit") - if valSlice == nil { + var models []BusinessModel + err := sessions.Get(ctx).Decode("businessEdit", &models) + if err != nil { + ctx.StopWithError(iris.StatusInternalServerError, err) + return + } + + if models == nil { ctx.HTML("please navigate to the /set-immutable first") return } - firstModel := valSlice.([]BusinessModel)[0] + firstModel := models[0] // businessGet[0].Name is equal to Edward initially if firstModel.Name != "Edward" { panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable") diff --git a/go.mod b/go.mod index 57533859..fd3c1ee9 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,18 @@ module github.com/kataras/iris/v12 -go 1.14 +go 1.15 require ( github.com/BurntSushi/toml v0.3.1 github.com/CloudyKit/jet/v5 v5.0.3 github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 - github.com/andybalholm/brotli v1.0.1-0.20200619015827-c3da72aa01ed + github.com/andybalholm/brotli v1.0.1 github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible github.com/dgraph-io/badger/v2 v2.2007.2 github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 github.com/fatih/structs v1.1.0 github.com/flosch/pongo2/v4 v4.0.0 - github.com/gomodule/redigo v1.8.2 + github.com/go-redis/redis/v8 v8.2.3 github.com/google/uuid v1.1.2 github.com/hashicorp/go-version v1.2.1 github.com/iris-contrib/httpexpect/v2 v2.0.5 @@ -25,20 +25,19 @@ require ( github.com/kataras/pio v0.0.10 github.com/kataras/sitemap v0.0.5 github.com/kataras/tunnel v0.0.2 - github.com/klauspost/compress v1.11.0 + github.com/klauspost/compress v1.11.1 github.com/mailru/easyjson v0.7.6 - github.com/mediocregopher/radix/v3 v3.5.2 github.com/microcosm-cc/bluemonday v1.0.4 github.com/russross/blackfriday/v2 v2.0.1 github.com/schollz/closestmatch v2.1.0+incompatible github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 - github.com/tdewolff/minify/v2 v2.9.5 + github.com/tdewolff/minify/v2 v2.9.7 github.com/vmihailenco/msgpack/v5 v5.0.0-beta.1 github.com/yosssi/ace v0.0.5 go.etcd.io/bbolt v1.3.5 - golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a - golang.org/x/net v0.0.0-20200927032502-5d4f70055728 - golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 + golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c + golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f golang.org/x/text v0.3.3 golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e google.golang.org/protobuf v1.25.0 diff --git a/sessions/database.go b/sessions/database.go index 0f590062..79a80f80 100644 --- a/sessions/database.go +++ b/sessions/database.go @@ -2,6 +2,7 @@ package sessions import ( "errors" + "reflect" "sync" "time" @@ -40,20 +41,24 @@ type Database interface { OnUpdateExpiration(sid string, newExpires time.Duration) error // Set sets a key value of a specific session. // The "immutable" input argument depends on the store, it may not implement it at all. - Set(sid string, lifetime *LifeTime, key string, value interface{}, immutable bool) + Set(sid string, key string, value interface{}, ttl time.Duration, immutable bool) error // Get retrieves a session value based on the key. Get(sid string, key string) interface{} + // Decode binds the "outPtr" to the value associated to the provided "key". + Decode(sid, key string, outPtr interface{}) error // Visit loops through all session keys and values. - Visit(sid string, cb func(key string, value interface{})) + Visit(sid string, cb func(key string, value interface{})) error // Len returns the length of the session's entries (keys). Len(sid string) int // Delete removes a session key value based on its key. Delete(sid string, key string) (deleted bool) // Clear removes all session key values but it keeps the session entry. - Clear(sid string) + Clear(sid string) error // 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. - Release(sid string) + Release(sid string) error + // Close should terminate the database connection. It's called automatically on interrupt signals. + Close() error } type mem struct { @@ -78,10 +83,12 @@ func (s *mem) Acquire(sid string, expires time.Duration) LifeTime { func (s *mem) OnUpdateExpiration(string, time.Duration) error { return nil } // immutable depends on the store, it may not implement it at all. -func (s *mem) Set(sid string, lifetime *LifeTime, key string, value interface{}, immutable bool) { +func (s *mem) Set(sid string, key string, value interface{}, _ time.Duration, immutable bool) error { s.mu.RLock() s.values[sid].Save(key, value, immutable) s.mu.RUnlock() + + return nil } func (s *mem) Get(sid string, key string) interface{} { @@ -92,8 +99,19 @@ func (s *mem) Get(sid string, key string) interface{} { return v } -func (s *mem) Visit(sid string, cb func(key string, value interface{})) { +func (s *mem) Decode(sid string, key string, outPtr interface{}) error { + s.mu.RLock() + v := s.values[sid].Get(key) + s.mu.RUnlock() + if v != nil { + reflect.ValueOf(outPtr).Set(reflect.ValueOf(v)) + } + return nil +} + +func (s *mem) Visit(sid string, cb func(key string, value interface{})) error { s.values[sid].Visit(cb) + return nil } func (s *mem) Len(sid string) int { @@ -111,14 +129,20 @@ func (s *mem) Delete(sid string, key string) (deleted bool) { return } -func (s *mem) Clear(sid string) { +func (s *mem) Clear(sid string) error { s.mu.Lock() s.values[sid].Reset() s.mu.Unlock() + + return nil } -func (s *mem) Release(sid string) { +func (s *mem) Release(sid string) error { s.mu.Lock() delete(s.values, sid) s.mu.Unlock() + + return nil } + +func (s *mem) Close() error { return nil } diff --git a/sessions/provider.go b/sessions/provider.go index 35e724c1..0ecdb3bd 100644 --- a/sessions/provider.go +++ b/sessions/provider.go @@ -77,6 +77,7 @@ func (p *provider) newSession(man *Sessions, sid string, expires time.Duration) // Init creates the session and returns it func (p *provider) Init(man *Sessions, sid string, expires time.Duration) *Session { newSession := p.newSession(man, sid, expires) + newSession.isNew = true p.mu.Lock() p.sessions[sid] = newSession p.mu.Unlock() @@ -118,13 +119,16 @@ func (p *provider) UpdateExpiration(sid string, expires time.Duration) error { // Read returns the store which sid parameter belongs func (p *provider) Read(man *Sessions, sid string, expires time.Duration) *Session { p.mu.RLock() - if sess, found := p.sessions[sid]; found { + sess, found := p.sessions[sid] + p.mu.RUnlock() + if found { + sess.mu.Lock() + sess.isNew = false + sess.mu.Unlock() sess.runFlashGC() // run the flash messages GC, new request here of existing session - p.mu.RUnlock() return sess } - p.mu.RUnlock() return p.Init(man, sid, expires) // if not found create new } diff --git a/sessions/session.go b/sessions/session.go index 204d2261..908e6a0b 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -53,7 +53,7 @@ func (s *Session) ID() string { return s.sid } -// IsNew returns true if this session is +// IsNew returns true if this session is just // created by the current application's process. func (s *Session) IsNew() bool { return s.isNew @@ -64,6 +64,11 @@ func (s *Session) Get(key string) interface{} { return s.provider.db.Get(s.sid, key) } +// Decode binds the given "outPtr" to the value associated to the provided "key". +func (s *Session) Decode(key string, outPtr interface{}) error { + return s.provider.db.Decode(s.sid, key, outPtr) +} + // when running on the session manager removes any 'old' flash messages. func (s *Session) runFlashGC() { s.mu.Lock() @@ -517,11 +522,7 @@ func (s *Session) Len() int { } func (s *Session) set(key string, value interface{}, immutable bool) { - s.provider.db.Set(s.sid, s.Lifetime, key, value, immutable) - - s.mu.Lock() - s.isNew = false - s.mu.Unlock() + s.provider.db.Set(s.sid, key, value, s.Lifetime.DurationUntilExpiration(), immutable) } // Set fills the session with an entry "value", based on its "key". @@ -569,12 +570,6 @@ func (s *Session) SetFlash(key string, value interface{}) { // returns true if actually something was removed. func (s *Session) Delete(key string) bool { removed := s.provider.db.Delete(s.sid, key) - if removed { - s.mu.Lock() - s.isNew = false - s.mu.Unlock() - } - return removed } @@ -587,10 +582,7 @@ func (s *Session) DeleteFlash(key string) { // Clear removes all entries. func (s *Session) Clear() { - s.mu.Lock() s.provider.db.Clear(s.sid) - s.isNew = false - s.mu.Unlock() } // ClearFlashes removes all flash messages. diff --git a/sessions/sessiondb/badger/database.go b/sessions/sessiondb/badger/database.go index 512fc462..2864fc4f 100644 --- a/sessions/sessiondb/badger/database.go +++ b/sessions/sessiondb/badger/database.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "os" - "runtime" "sync/atomic" "time" @@ -71,7 +70,7 @@ func New(directoryPath string) (*Database, error) { func NewFromDB(service *badger.DB) *Database { db := &Database{Service: service} - runtime.SetFinalizer(db, closeDB) + // runtime.SetFinalizer(db, closeDB) return db } @@ -127,25 +126,35 @@ func makeKey(sid, key string) []byte { // 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) { +func (db *Database) Set(sid string, key string, value interface{}, ttl time.Duration, immutable bool) error { valueBytes, err := sessions.DefaultTranscoder.Marshal(value) if err != nil { db.logger.Error(err) - return + return err } err = db.Service.Update(func(txn *badger.Txn) error { - dur := lifetime.DurationUntilExpiration() - return txn.SetEntry(badger.NewEntry(makeKey(sid, key), valueBytes).WithTTL(dur)) + return txn.SetEntry(badger.NewEntry(makeKey(sid, key), valueBytes).WithTTL(ttl)) }) if err != nil { db.logger.Error(err) } + + return err } // Get retrieves a session value based on the key. func (db *Database) Get(sid string, key string) (value interface{}) { + if err := db.Decode(sid, key, &value); err == nil { + return value + } + + return nil +} + +// Decode binds the "outPtr" to the value associated to the provided "key". +func (db *Database) Decode(sid, key string, outPtr interface{}) error { err := db.Service.View(func(txn *badger.Txn) error { item, err := txn.Get(makeKey(sid, key)) if err != nil { @@ -153,16 +162,15 @@ func (db *Database) Get(sid string, key string) (value interface{}) { } return item.Value(func(valueBytes []byte) error { - return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) + return sessions.DefaultTranscoder.Unmarshal(valueBytes, outPtr) }) }) if err != nil && err != badger.ErrKeyNotFound { db.logger.Error(err) - return nil } - return + return err } // validSessionItem reports whether the current iterator's item key @@ -172,7 +180,7 @@ func validSessionItem(key, prefix []byte) bool { } // Visit loops through all session keys and values. -func (db *Database) Visit(sid string, cb func(key string, value interface{})) { +func (db *Database) Visit(sid string, cb func(key string, value interface{})) error { prefix := makePrefix(sid) txn := db.Service.NewTransaction(false) @@ -199,11 +207,13 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) { }) if err != nil { db.logger.Errorf("[sessionsdb.badger.Visit] %v", err) - continue + return err } cb(string(bytes.TrimPrefix(key, prefix)), value) } + + return nil } var iterOptionsNoValues = badger.IteratorOptions{ @@ -247,7 +257,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) { +func (db *Database) Clear(sid string) error { prefix := makePrefix(sid) txn := db.Service.NewTransaction(true) @@ -260,24 +270,33 @@ func (db *Database) Clear(sid string) { key := iter.Item().Key() if err := txn.Delete(key); err != nil { db.logger.Warnf("Database.Clear: %s: %v", key, err) - continue + return err } } + + return nil } // 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) { +func (db *Database) Release(sid string) error { // clear all $sid-$key. - db.Clear(sid) + err := db.Clear(sid) + if err != nil { + return err + } // and remove the $sid. txn := db.Service.NewTransaction(true) - if err := txn.Delete([]byte(sid)); err != nil { + if err = txn.Delete([]byte(sid)); err != nil { db.logger.Warnf("Database.Release.Delete: %s: %v", sid, err) + return err } - if err := txn.Commit(); err != nil { + if err = txn.Commit(); err != nil { db.logger.Debugf("Database.Release.Commit: %s: %v", sid, err) + return err } + + return nil } // Close shutdowns the badger connection. diff --git a/sessions/sessiondb/boltdb/database.go b/sessions/sessiondb/boltdb/database.go index 9899fb62..a2b2335c 100644 --- a/sessions/sessiondb/boltdb/database.go +++ b/sessions/sessiondb/boltdb/database.go @@ -4,7 +4,6 @@ import ( "errors" "os" "path/filepath" - "runtime" "time" "github.com/kataras/iris/v12/sessions" @@ -79,7 +78,7 @@ func NewFromDB(service *bolt.DB, bucketName string) (*Database, error) { db := &Database{table: bucket, Service: service} - runtime.SetFinalizer(db, closeDB) + // runtime.SetFinalizer(db, closeDB) return db, db.cleanup() } @@ -254,11 +253,11 @@ func makeKey(key string) []byte { // Set sets a key value of a specific session. // Ignore the "immutable". -func (db *Database) Set(sid string, _ *sessions.LifeTime, key string, value interface{}, immutable bool) { +func (db *Database) Set(sid string, key string, value interface{}, ttl time.Duration, immutable bool) error { valueBytes, err := sessions.DefaultTranscoder.Marshal(value) if err != nil { db.logger.Debug(err) - return + return err } err = db.Service.Update(func(tx *bolt.Tx) error { @@ -277,10 +276,21 @@ func (db *Database) Set(sid string, _ *sessions.LifeTime, key string, value inte if err != nil { db.logger.Debug(err) } + + return err } // Get retrieves a session value based on the key. func (db *Database) Get(sid string, key string) (value interface{}) { + if err := db.Decode(sid, key, &value); err == nil { + return value + } + + return nil +} + +// Decode binds the "outPtr" to the value associated to the provided "key". +func (db *Database) Decode(sid, key string, outPtr interface{}) error { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { @@ -292,17 +302,17 @@ func (db *Database) Get(sid string, key string) (value interface{}) { return nil } - return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) + return sessions.DefaultTranscoder.Unmarshal(valueBytes, outPtr) }) if err != nil { db.logger.Debugf("session '%s' key '%s' cannot be retrieved: %v", sid, key, err) } - return + return err } // Visit loops through all session keys and values. -func (db *Database) Visit(sid string, cb func(key string, value interface{})) { +func (db *Database) Visit(sid string, cb func(key string, value interface{})) error { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { @@ -324,17 +334,19 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) { if err != nil { db.logger.Debugf("Database.Visit: %s: %v", sid, err) } + + return err } // Len returns the length of the session's entries (keys). -func (db *Database) Len(sid string) (n int) { +func (db *Database) Len(sid string) (n int64) { err := db.Service.View(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { return nil } - n = b.Stats().KeyN + n = int64(b.Stats().KeyN) return nil }) @@ -360,7 +372,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) { +func (db *Database) Clear(sid string) error { err := db.Service.Update(func(tx *bolt.Tx) error { b := db.getBucketForSession(tx, sid) if b == nil { @@ -375,11 +387,13 @@ func (db *Database) Clear(sid string) { if err != nil { db.logger.Debugf("Database.Clear: %s: %v", sid, err) } + + return err } // 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) { +func (db *Database) Release(sid string) error { err := db.Service.Update(func(tx *bolt.Tx) error { // delete the session bucket. b := db.getBucket(tx) @@ -393,6 +407,8 @@ func (db *Database) Release(sid string) { if err != nil { db.logger.Debugf("Database.Release: %s: %v", sid, err) } + + return err } // Close shutdowns the BoltDB connection. diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go index 51a0d94b..a9fa2589 100644 --- a/sessions/sessiondb/redis/database.go +++ b/sessions/sessiondb/redis/database.go @@ -3,7 +3,7 @@ package redis import ( "crypto/tls" "errors" - "strings" + "fmt" "time" "github.com/kataras/iris/v12/sessions" @@ -18,8 +18,6 @@ const ( DefaultRedisAddr = "127.0.0.1:6379" // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second DefaultRedisTimeout = time.Duration(30) * time.Second - // DefaultDelim ths redis delim option, "-". - DefaultDelim = "-" ) // Config the redis configuration used inside sessions @@ -31,31 +29,36 @@ type Config struct { // Defaults to "127.0.0.1:6379". Addr string // Clusters a list of network addresses for clusters. - // If not empty "Addr" is ignored. - // Currently only Radix() Driver supports it. + // If not empty "Addr" is ignored and Redis clusters feature is used instead. Clusters []string - // Password string .If no password then no 'AUTH'. Defaults to "". + // Use the specified Username to authenticate the current connection + // with one of the connections defined in the ACL list when connecting + // to a Redis 6.0 instance, or greater, that is using the Redis ACL system. + Username string + // Optional password. Must match the password specified in the + // requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower), + // or the User Password when connecting to a Redis 6.0 instance, or greater, + // that is using the Redis ACL system. Password string // If Database is empty "" then no 'SELECT'. Defaults to "". Database string - // MaxActive. Defaults to 10. + // Maximum number of socket connections. + // Default is 10 connections per every CPU as reported by runtime.NumCPU. MaxActive int // Timeout for connect, write and read, defaults to 30 seconds, 0 means no timeout. Timeout time.Duration // Prefix "myprefix-for-this-website". Defaults to "". Prefix string - // Delim the delimiter for the keys on the sessiondb. Defaults to "-". - Delim string // TLSConfig will cause Dial to perform a TLS handshake using the provided // config. If is nil then no TLS is used. // See https://golang.org/pkg/crypto/tls/#Config TLSConfig *tls.Config - // Driver supports `Redigo()` or `Radix()` go clients for redis. - // Configure each driver by the return value of their constructors. + // A Driver should support be a go client for redis communication. + // It can be set to a custom one or a mock one (for testing). // - // Defaults to `Redigo()`. + // Defaults to `GoRedis()`. Driver Driver } @@ -64,14 +67,14 @@ func DefaultConfig() Config { return Config{ Network: DefaultRedisNetwork, Addr: DefaultRedisAddr, + Username: "", Password: "", Database: "", MaxActive: 10, Timeout: DefaultRedisTimeout, Prefix: "", - Delim: DefaultDelim, TLSConfig: nil, - Driver: Redigo(), + Driver: GoRedis(), } } @@ -83,7 +86,7 @@ type Database struct { var _ sessions.Database = (*Database)(nil) -// New returns a new redis database. +// New returns a new redis sessions database. func New(cfg ...Config) *Database { c := DefaultConfig() if len(cfg) > 0 { @@ -101,16 +104,8 @@ func New(cfg ...Config) *Database { c.Addr = DefaultRedisAddr } - if c.MaxActive == 0 { - c.MaxActive = 10 - } - - if c.Delim == "" { - c.Delim = DefaultDelim - } - if c.Driver == nil { - c.Driver = Redigo() + c.Driver = GoRedis() } } @@ -127,93 +122,108 @@ func New(cfg ...Config) *Database { return db } -// Config returns the configuration for the redis server bridge, you can change them. -func (db *Database) Config() *Config { - return &db.c // 6 Aug 2019 - keep that for no breaking change. -} - // SetLogger sets the logger once before server ran. // By default the Iris one is injected. func (db *Database) SetLogger(logger *golog.Logger) { db.logger = logger } +func (db *Database) makeSID(sid string) string { + return db.c.Prefix + sid +} + +// SessionIDKey the session ID stored to the redis session itself. +const SessionIDKey = "session_id" + // 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) sessions.LifeTime { - key := db.makeKey(sid, "") - seconds, hasExpiration, found := db.c.Driver.TTL(key) - if !found { - // fmt.Printf("db.Acquire expires: %s. Seconds: %v\n", expires, expires.Seconds()) - // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. - if err := db.c.Driver.Set(key, sid, int64(expires.Seconds())); err != nil { + sidKey := db.makeSID(sid) + if !db.c.Driver.Exists(sidKey) { + if err := db.Set(sidKey, SessionIDKey, sid, 0, false); err != nil { db.logger.Debug(err) + } else if expires > 0 { + if err := db.c.Driver.UpdateTTL(sidKey, expires); err != nil { + db.logger.Debug(err) + } } return sessions.LifeTime{} // session manager will handle the rest. } - if !hasExpiration { - return sessions.LifeTime{} - } - - return sessions.LifeTime{Time: time.Now().Add(time.Duration(seconds) * time.Second)} + untilExpire := db.c.Driver.TTL(sidKey) + return sessions.LifeTime{Time: time.Now().Add(untilExpire)} } // OnUpdateExpiration will re-set the database's session's entry ttl. // https://redis.io/commands/expire#refreshing-expires func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { - return db.c.Driver.UpdateTTLMany(db.makeKey(sid, ""), int64(newExpires.Seconds())) -} - -func (db *Database) makeKey(sid, key string) string { - if key == "" { - return db.c.Prefix + sid - } - return db.c.Prefix + sid + db.c.Delim + key + return db.c.Driver.UpdateTTL(db.makeSID(sid), newExpires) } // 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) { +func (db *Database) Set(sid string, key string, value interface{}, _ time.Duration, _ bool) error { valueBytes, err := sessions.DefaultTranscoder.Marshal(value) if err != nil { db.logger.Error(err) - return - } - - // fmt.Println("database.Set") - // fmt.Printf("lifetime.DurationUntilExpiration(): %s. Seconds: %v\n", lifetime.DurationUntilExpiration(), lifetime.DurationUntilExpiration().Seconds()) - if err = db.c.Driver.Set(db.makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { - db.logger.Debug(err) - } -} - -// Get retrieves a session value based on the key. -func (db *Database) Get(sid string, key string) (value interface{}) { - db.get(db.makeKey(sid, key), &value) - return -} - -func (db *Database) get(key string, outPtr interface{}) error { - data, err := db.c.Driver.Get(key) - if err != nil { - // not found. return err } - if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil { - db.logger.Debugf("unable to unmarshal value of key: '%s': %v", key, err) + if err = db.c.Driver.Set(db.makeSID(sid), key, valueBytes); err != nil { + db.logger.Debug(err) return err } return nil } -func (db *Database) keys(sid string) []string { - keys, err := db.c.Driver.GetKeys(db.makeKey(sid, "")) +// Get retrieves a session value based on the key. +func (db *Database) Get(fullSID string, key string) (value interface{}) { + if err := db.Decode(fullSID, key, &value); err == nil { + return value + } + + return nil +} + +// Decode binds the "outPtr" to the value associated to the provided "key". +func (db *Database) Decode(sid, key string, outPtr interface{}) error { + data, err := db.c.Driver.Get(sid, key) if err != nil { - db.logger.Debugf("unable to get all redis keys of session '%s': %v", sid, err) + // not found. + return err + } + + if err = db.decodeValue(data, outPtr); err != nil { + db.logger.Debugf("unable to unmarshal value of key: '%s%s': %v", sid, key, err) + return err + } + + return nil +} + +func (db *Database) decodeValue(val interface{}, outPtr interface{}) error { + if val == nil { + return nil + } + + switch data := val.(type) { + case []byte: + // this is the most common type, as we save all values as []byte, + // the only exception is where the value is string on HGetAll command. + return sessions.DefaultTranscoder.Unmarshal(data, outPtr) + case string: + return sessions.DefaultTranscoder.Unmarshal([]byte(data), outPtr) + default: + return fmt.Errorf("unknown value type of %T", data) + } +} + +func (db *Database) keys(fullSID string) []string { + keys, err := db.c.Driver.GetKeys(fullSID) + if err != nil { + db.logger.Debugf("unable to get all redis keys of session '%s': %v", fullSID, err) return nil } @@ -221,24 +231,33 @@ func (db *Database) keys(sid string) []string { } // Visit loops through all session keys and values. -func (db *Database) Visit(sid string, cb func(key string, value interface{})) { - keys := db.keys(sid) - for _, key := range keys { - var value interface{} // new value each time, we don't know what user will do in "cb". - db.get(key, &value) - key = strings.TrimPrefix(key, db.c.Prefix+sid+db.c.Delim) - cb(key, value) +func (db *Database) Visit(sid string, cb func(key string, value interface{})) error { + kv, err := db.c.Driver.GetAll(db.makeSID(sid)) + if err != nil { + return err } + + for k, v := range kv { + var value interface{} // new value each time, we don't know what user will do in "cb". + if err = db.decodeValue(v, &value); err != nil { + db.logger.Debugf("unable to decode %s:%s: %v", sid, k, err) + return err + } + + cb(k, value) + } + + return nil } // Len returns the length of the session's entries (keys). -func (db *Database) Len(sid string) (n int) { - return len(db.keys(sid)) +func (db *Database) Len(sid string) int { + return db.c.Driver.Len(sid) } // Delete removes a session key value based on its key. func (db *Database) Delete(sid string, key string) (deleted bool) { - err := db.c.Driver.Delete(db.makeKey(sid, key)) + err := db.c.Driver.Delete(db.makeSID(sid), key) if err != nil { db.logger.Error(err) } @@ -246,25 +265,30 @@ 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) { - keys := db.keys(sid) +func (db *Database) Clear(sid string) error { + keys := db.keys(db.makeSID(sid)) for _, key := range keys { - if err := db.c.Driver.Delete(key); err != nil { + if key == SessionIDKey { + continue + } + if err := db.c.Driver.Delete(sid, key); err != nil { db.logger.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err) + return err } } + + return nil } // 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) { - // clear all $sid-$key. - db.Clear(sid) - // and remove the $sid. - err := db.c.Driver.Delete(db.c.Prefix + sid) +func (db *Database) Release(sid string) error { + err := db.c.Driver.Delete(db.makeSID(sid), "") if err != nil { db.logger.Debugf("Database.Release.Driver.Delete: %s: %v", sid, err) } + + return err } // Close terminates the redis connection. diff --git a/sessions/sessiondb/redis/driver.go b/sessions/sessiondb/redis/driver.go index 3aae3ab9..672f6b98 100644 --- a/sessions/sessiondb/redis/driver.go +++ b/sessions/sessiondb/redis/driver.go @@ -1,34 +1,30 @@ package redis +import "time" + // Driver is the interface which each supported redis client // should support in order to be used in the redis session database. type Driver interface { Connect(c Config) error PingPong() (bool, error) CloseConnection() error - Set(key string, value interface{}, secondsLifetime int64) error - Get(key string) (interface{}, error) - TTL(key string) (seconds int64, hasExpiration bool, found bool) - UpdateTTL(key string, newSecondsLifeTime int64) error - UpdateTTLMany(prefix string, newSecondsLifeTime int64) error - GetAll() (interface{}, error) - GetKeys(prefix string) ([]string, error) - Delete(key string) error + Set(sid, key string, value interface{}) error + Get(sid, key string) (interface{}, error) + Exists(sid string) bool + TTL(sid string) time.Duration + UpdateTTL(sid string, newLifetime time.Duration) error + GetAll(sid string) (map[string]string, error) + GetKeys(sid string) ([]string, error) + Len(sid string) int + Delete(sid, key string) error } var ( - _ Driver = (*RedigoDriver)(nil) - _ Driver = (*RadixDriver)(nil) + _ Driver = (*GoRedisDriver)(nil) ) -// Redigo returns the driver for the redigo go redis client. -// Which is the default one. -// You can customize further any specific driver's properties. -func Redigo() *RedigoDriver { - return &RedigoDriver{} -} - -// Radix returns the driver for the radix go redis client. -func Radix() *RadixDriver { - return &RadixDriver{} +// GoRedis returns the default Driver for the redis sessions database +// It's the go-redis client. Learn more at: https://github.com/go-redis/redis. +func GoRedis() *GoRedisDriver { + return &GoRedisDriver{} } diff --git a/sessions/sessiondb/redis/driver_goredis.go b/sessions/sessiondb/redis/driver_goredis.go new file mode 100644 index 00000000..bc2d0a22 --- /dev/null +++ b/sessions/sessiondb/redis/driver_goredis.go @@ -0,0 +1,195 @@ +package redis + +import ( + stdContext "context" + "io" + "strconv" + "time" + + "github.com/go-redis/redis/v8" +) + +type ( + // Options is just a type alias for the go-redis Client Options. + Options = redis.Options + // ClusterOptions is just a type alias for the go-redis Cluster Client Options. + ClusterOptions = redis.ClusterOptions +) + +// GoRedisClient is the interface which both +// go-redis' Client and Cluster Client implements. +type GoRedisClient interface { + redis.Cmdable // Commands. + io.Closer // CloseConnection. +} + +// GoRedisDriver implements the Sessions Database Driver +// for the go-redis redis driver. See driver.go file. +type GoRedisDriver struct { + // Both Client and ClusterClient implements this interface. + client GoRedisClient + // Customize any go-redis fields manually + // before Connect. + ClientOptions Options + ClusterOptions ClusterOptions +} + +var defaultContext = stdContext.Background() + +func (r *GoRedisDriver) mergeClientOptions(c Config) *Options { + opts := r.ClientOptions + if opts.Addr == "" { + opts.Addr = c.Addr + } + + if opts.Username == "" { + opts.Username = c.Username + } + + if opts.Password == "" { + opts.Username = c.Password + } + + if opts.DB == 0 { + opts.DB, _ = strconv.Atoi(c.Database) + } + + if opts.ReadTimeout == 0 { + opts.ReadTimeout = c.Timeout + } + + if opts.WriteTimeout == 0 { + opts.WriteTimeout = c.Timeout + } + + if opts.Network == "" { + opts.Network = c.Network + } + + if opts.TLSConfig == nil { + opts.TLSConfig = c.TLSConfig + } + + if opts.PoolSize == 0 { + opts.PoolSize = c.MaxActive + } + + return &opts +} + +func (r *GoRedisDriver) mergeClusterOptions(c Config) *ClusterOptions { + opts := r.ClusterOptions + + if opts.Username == "" { + opts.Username = c.Username + } + + if opts.Password == "" { + opts.Username = c.Password + } + + if opts.ReadTimeout == 0 { + opts.ReadTimeout = c.Timeout + } + + if opts.WriteTimeout == 0 { + opts.WriteTimeout = c.Timeout + } + + if opts.TLSConfig == nil { + opts.TLSConfig = c.TLSConfig + } + + if opts.PoolSize == 0 { + opts.PoolSize = c.MaxActive + } + + if len(opts.Addrs) == 0 { + opts.Addrs = c.Clusters + } + + return &opts +} + +// Connect initializes the redis client. +func (r *GoRedisDriver) Connect(c Config) error { + if len(c.Clusters) > 0 { + r.client = redis.NewClusterClient(r.mergeClusterOptions(c)) + } else { + r.client = redis.NewClient(r.mergeClientOptions(c)) + } + + return nil +} + +// PingPong sends a ping message and reports whether +// the PONG message received successfully. +func (r *GoRedisDriver) PingPong() (bool, error) { + pong, err := r.client.Ping(defaultContext).Result() + return pong == "PONG", err +} + +// CloseConnection terminates the underline redis connection. +func (r *GoRedisDriver) CloseConnection() error { + return r.client.Close() +} + +// Set stores a "value" based on the session's "key". +// The value should be type of []byte, so unmarshal can happen. +func (r *GoRedisDriver) Set(sid, key string, value interface{}) error { + return r.client.HSet(defaultContext, sid, key, value).Err() +} + +// Get returns the associated value of the session's given "key". +func (r *GoRedisDriver) Get(sid, key string) (interface{}, error) { + return r.client.HGet(defaultContext, sid, key).Bytes() +} + +// Exists reports whether a session exists or not. +func (r *GoRedisDriver) Exists(sid string) bool { + n, err := r.client.Exists(defaultContext, sid).Result() + if err != nil { + return false + } + + return n > 0 +} + +// TTL returns any TTL value of the session. +func (r *GoRedisDriver) TTL(sid string) time.Duration { + dur, err := r.client.TTL(defaultContext, sid).Result() + if err != nil { + return 0 + } + + return dur +} + +// UpdateTTL sets expiration duration of the session. +func (r *GoRedisDriver) UpdateTTL(sid string, newLifetime time.Duration) error { + _, err := r.client.Expire(defaultContext, sid, newLifetime).Result() + return err +} + +// GetAll returns all the key values under the session. +func (r *GoRedisDriver) GetAll(sid string) (map[string]string, error) { + return r.client.HGetAll(defaultContext, sid).Result() +} + +// GetKeys returns all keys under the session. +func (r *GoRedisDriver) GetKeys(sid string) ([]string, error) { + return r.client.HKeys(defaultContext, sid).Result() +} + +// Len returns the total length of key-values of the session. +func (r *GoRedisDriver) Len(sid string) int { + return int(r.client.HLen(defaultContext, sid).Val()) +} + +// Delete removes a value from the redis store. +func (r *GoRedisDriver) Delete(sid, key string) error { + if key == "" { + return r.client.Del(defaultContext, sid).Err() + } + return r.client.HDel(defaultContext, sid, key).Err() +} diff --git a/sessions/sessiondb/redis/driver_radix.go b/sessions/sessiondb/redis/driver_radix.go deleted file mode 100644 index 663ad086..00000000 --- a/sessions/sessiondb/redis/driver_radix.go +++ /dev/null @@ -1,321 +0,0 @@ -package redis - -import ( - "bufio" - "errors" - "fmt" - "strconv" - - "github.com/mediocregopher/radix/v3" - "github.com/mediocregopher/radix/v3/resp/resp2" -) - -// radixPool an interface to complete both *radix.Pool and *radix.Cluster. -type radixPool interface { - Do(a radix.Action) error - Close() error -} - -// RadixDriver the Redis service based on the radix go client, -// contains the config and the redis pool. -type RadixDriver struct { - // Connected is true when the Service has already connected - Connected bool - // Config the read-only redis database config. - Config Config - pool radixPool -} - -// Connect connects to the redis, called only once -func (r *RadixDriver) Connect(c Config) error { - if c.Timeout < 0 { - c.Timeout = DefaultRedisTimeout - } - - if c.Network == "" { - c.Network = DefaultRedisNetwork - } - - if c.Addr == "" { - c.Addr = DefaultRedisAddr - } - - if c.MaxActive == 0 { - c.MaxActive = 10 - } - - if c.Delim == "" { - c.Delim = DefaultDelim - } - - var options []radix.DialOpt - - if c.TLSConfig != nil { - options = append(options, radix.DialUseTLS(c.TLSConfig)) - } - - if c.Password != "" { - options = append(options, radix.DialAuthPass(c.Password)) - } - - if c.Timeout > 0 { - options = append(options, radix.DialTimeout(c.Timeout)) - } - - if c.Database != "" { // *dialOpts.selectDb is not exported on the 3rd-party library, - // but on its `DialSelectDB` option it does this: - // do.selectDB = strconv.Itoa(db) -> (string to int) - // so we can pass that string as int and it should work. - dbIndex, err := strconv.Atoi(c.Database) - if err == nil { - options = append(options, radix.DialSelectDB(dbIndex)) - } - - } - - var connFunc radix.ConnFunc - - /* Note(@kataras): according to #1545 the below does NOT work, and we should - use the Cluster instance itself to fire requests. - We need a separate `radix.Cluster` instance to do the calls, - fortunally both Pool and Cluster implement the same Do and Close methods we need, - so a new `radixPool` interface to remove any dupl code is used instead. - - if len(c.Clusters) > 0 { - cluster, err := radix.NewCluster(c.Clusters) - if err != nil { - // maybe an - // ERR This instance has cluster support disabled - return err - } - - connFunc = func(network, addr string) (radix.Conn, error) { - topo := cluster.Topo() - node := topo[rand.Intn(len(topo))] - return radix.Dial(c.Network, node.Addr, options...) - } - } else { - */ - connFunc = func(network, addr string) (radix.Conn, error) { - return radix.Dial(c.Network, c.Addr, options...) - } - - var pool radixPool - - if len(c.Clusters) > 0 { - poolFunc := func(network, addr string) (radix.Client, error) { - return radix.NewPool(network, addr, c.MaxActive, radix.PoolConnFunc(connFunc)) - } - - cluster, err := radix.NewCluster(c.Clusters, radix.ClusterPoolFunc(poolFunc)) - if err != nil { - return err - } - - pool = cluster - } else { - p, err := radix.NewPool(c.Network, c.Addr, c.MaxActive, radix.PoolConnFunc(connFunc)) - if err != nil { - return err - } - - pool = p - } - - r.Connected = true - r.pool = pool - r.Config = c - return nil -} - -// PingPong sends a ping and receives a pong, if no pong received then returns false and filled error -func (r *RadixDriver) PingPong() (bool, error) { - var msg string - err := r.pool.Do(radix.Cmd(&msg, "PING")) - if err != nil { - return false, err - } - return (msg == "PONG"), nil -} - -// CloseConnection closes the redis connection. -func (r *RadixDriver) CloseConnection() error { - if r.pool != nil { - return r.pool.Close() - } - return ErrRedisClosed -} - -// Set sets a key-value to the redis store. -// The expiration is setted by the secondsLifetime. -func (r *RadixDriver) Set(key string, value interface{}, secondsLifetime int64) error { - var cmd radix.CmdAction - // if has expiration, then use the "EX" to delete the key automatically. - if secondsLifetime > 0 { - cmd = radix.FlatCmd(nil, "SETEX", key, secondsLifetime, value) - } else { - cmd = radix.FlatCmd(nil, "SET", key, value) // MSET same performance... - } - - return r.pool.Do(cmd) -} - -// Get returns value, err by its key -// returns nil and a filled error if something bad happened. -func (r *RadixDriver) Get(key string /* full key */) (interface{}, error) { - var redisVal interface{} - mn := radix.MaybeNil{Rcv: &redisVal} - - err := r.pool.Do(radix.Cmd(&mn, "GET", key)) - if err != nil { - return nil, err - } - if mn.Nil { - return nil, fmt.Errorf("%s: %w", key, ErrKeyNotFound) - } - return redisVal, nil -} - -// TTL returns the seconds to expire, if the key has expiration and error if action failed. -// Read more at: https://redis.io/commands/ttl -func (r *RadixDriver) TTL(key string) (seconds int64, hasExpiration bool, found bool) { - var redisVal interface{} - err := r.pool.Do(radix.Cmd(&redisVal, "TTL", key)) - if err != nil { - return -2, false, false - } - seconds = redisVal.(int64) - // if -1 means the key has unlimited life time. - hasExpiration = seconds > -1 - // if -2 means key does not exist. - found = seconds != -2 - return -} - -func (r *RadixDriver) updateTTLConn(key string /* full key */, newSecondsLifeTime int64) error { - var reply int - err := r.pool.Do(radix.FlatCmd(&reply, "EXPIRE", key, newSecondsLifeTime)) - if err != nil { - return err - } - - // https://redis.io/commands/expire#return-value - // - // 1 if the timeout was set. - // 0 if key does not exist. - - if reply == 1 { - return nil - } else if reply == 0 { - return fmt.Errorf("unable to update expiration, the key '%s' was stored without ttl", key) - } // do not check for -1. - - return nil -} - -// UpdateTTL will update the ttl of a key. -// Using the "EXPIRE" command. -// Read more at: https://redis.io/commands/expire#refreshing-expires -func (r *RadixDriver) UpdateTTL(key string, newSecondsLifeTime int64) error { - return r.updateTTLConn(key, newSecondsLifeTime) -} - -// UpdateTTLMany like `UpdateTTL` but for all keys starting with that "prefix", -// it is a bit faster operation if you need to update all sessions keys (although it can be even faster if we used hash but this will limit other features), -// look the `sessions/Database#OnUpdateExpiration` for example. -func (r *RadixDriver) UpdateTTLMany(prefix string /* prefix is the sid */, newSecondsLifeTime int64) error { - keys, err := r.getKeys("0", prefix, true) - if err != nil { - return err - } - - for _, key := range keys { - if err = r.updateTTLConn(key, newSecondsLifeTime); err != nil { // fail on first error. - return err - } - } - - return err -} - -// GetAll returns all redis entries using the "SCAN" command (2.8+). -func (r *RadixDriver) GetAll() (interface{}, error) { - var redisVal []interface{} - mn := radix.MaybeNil{Rcv: &redisVal} - err := r.pool.Do(radix.Cmd(&mn, "SCAN", strconv.Itoa(0))) // 0 -> cursor - if err != nil { - return nil, err - } - - if mn.Nil { - return nil, err - } - - return redisVal, nil -} - -type scanResult struct { - cur string - keys []string -} - -func (s *scanResult) UnmarshalRESP(br *bufio.Reader) error { - var ah resp2.ArrayHeader - if err := ah.UnmarshalRESP(br); err != nil { - return err - } else if ah.N != 2 { - return errors.New("not enough parts returned") - } - - var c resp2.BulkString - if err := c.UnmarshalRESP(br); err != nil { - return err - } - - s.cur = c.S - s.keys = s.keys[:0] - - return (resp2.Any{I: &s.keys}).UnmarshalRESP(br) -} - -func (r *RadixDriver) getKeys(cursor, prefix string, includeSID bool) ([]string, error) { - var res scanResult - - if !includeSID { - prefix += r.Config.Delim // delim can be used for fast matching of only keys. - } - pattern := prefix + "*" - - err := r.pool.Do(radix.Cmd(&res, "SCAN", cursor, "MATCH", pattern, "COUNT", "300000")) - if err != nil { - return nil, err - } - - if len(res.keys) == 0 { - return nil, nil - } - - keys := res.keys[0:] - if res.cur != "0" { - moreKeys, err := r.getKeys(res.cur, prefix, includeSID) - if err != nil { - return nil, err - } - - keys = append(keys, moreKeys...) - } - - return keys, nil -} - -// GetKeys returns all redis keys using the "SCAN" with MATCH command. -// Read more at: https://redis.io/commands/scan#the-match-option. -func (r *RadixDriver) GetKeys(prefix string) ([]string, error) { - return r.getKeys("0", prefix, false) -} - -// Delete removes redis entry by specific key -func (r *RadixDriver) Delete(key string) error { - err := r.pool.Do(radix.Cmd(nil, "DEL", key)) - return err -} diff --git a/sessions/sessiondb/redis/driver_redigo.go b/sessions/sessiondb/redis/driver_redigo.go deleted file mode 100644 index 0c3cbfdb..00000000 --- a/sessions/sessiondb/redis/driver_redigo.go +++ /dev/null @@ -1,347 +0,0 @@ -package redis - -import ( - "fmt" - "time" - - "github.com/gomodule/redigo/redis" -) - -// RedigoDriver is the redigo Redis go client, -// contains the config and the redis pool -type RedigoDriver struct { - // Config the read-only redis database config. - Config Config - - // Maximum number of idle connections in the pool. - MaxIdle int - - // Close connections after remaining idle for this duration. If the value - // is zero, then idle connections are not closed. Applications should set - // the timeout to a value less than the server's timeout. - IdleTimeout time.Duration - - // If Wait is true and the pool is at the MaxActive limit, then Get() waits - // for a connection to be returned to the pool before returning. - Wait bool - - // Connected is true when the Service has already connected - Connected bool - - pool *redis.Pool -} - -// PingPong sends a ping and receives a pong, if no pong received then returns false and filled error -func (r *RedigoDriver) PingPong() (bool, error) { - c := r.pool.Get() - defer c.Close() - msg, err := c.Do("PING") - if err != nil || msg == nil { - return false, err - } - return (msg == "PONG"), nil -} - -// CloseConnection closes the redis connection. -func (r *RedigoDriver) CloseConnection() error { - if r.pool != nil { - return r.pool.Close() - } - return ErrRedisClosed -} - -// Set sets a key-value to the redis store. -// The expiration is setted by the secondsLifetime. -func (r *RedigoDriver) Set(key string, value interface{}, secondsLifetime int64) (err error) { - c := r.pool.Get() - defer c.Close() - if c.Err() != nil { - return c.Err() - } - - // if has expiration, then use the "EX" to delete the key automatically. - if secondsLifetime > 0 { - _, err = c.Do("SETEX", key, secondsLifetime, value) - } else { - _, err = c.Do("SET", key, value) - } - - return -} - -// Get returns value, err by its key -// returns nil and a filled error if something bad happened. -func (r *RedigoDriver) Get(key string) (interface{}, error) { - c := r.pool.Get() - defer c.Close() - if err := c.Err(); err != nil { - return nil, err - } - - redisVal, err := c.Do("GET", key) - if err != nil { - return nil, err - } - if redisVal == nil { - return nil, fmt.Errorf("%s: %w", key, ErrKeyNotFound) - } - return redisVal, nil -} - -// TTL returns the seconds to expire, if the key has expiration and error if action failed. -// Read more at: https://redis.io/commands/ttl -func (r *RedigoDriver) TTL(key string) (seconds int64, hasExpiration bool, found bool) { - c := r.pool.Get() - defer c.Close() - redisVal, err := c.Do("TTL", key) - if err != nil { - return -2, false, false - } - seconds = redisVal.(int64) - // if -1 means the key has unlimited life time. - hasExpiration = seconds > -1 - // if -2 means key does not exist. - found = !(c.Err() != nil || seconds == -2) - return -} - -func (r *RedigoDriver) updateTTLConn(c redis.Conn, key string, newSecondsLifeTime int64) error { - reply, err := c.Do("EXPIRE", key, newSecondsLifeTime) - if err != nil { - return err - } - - // https://redis.io/commands/expire#return-value - // - // 1 if the timeout was set. - // 0 if key does not exist. - if hadTTLOrExists, ok := reply.(int); ok { - if hadTTLOrExists == 1 { - return nil - } else if hadTTLOrExists == 0 { - return fmt.Errorf("unable to update expiration, the key '%s' was stored without ttl", key) - } // do not check for -1. - } - - return nil -} - -// UpdateTTL will update the ttl of a key. -// Using the "EXPIRE" command. -// Read more at: https://redis.io/commands/expire#refreshing-expires -func (r *RedigoDriver) UpdateTTL(key string, newSecondsLifeTime int64) error { - c := r.pool.Get() - defer c.Close() - err := c.Err() - if err != nil { - return err - } - - return r.updateTTLConn(c, key, newSecondsLifeTime) -} - -// UpdateTTLMany like `UpdateTTL` but for all keys starting with that "prefix", -// it is a bit faster operation if you need to update all sessions keys (although it can be even faster if we used hash but this will limit other features), -// look the `sessions/Database#OnUpdateExpiration` for example. -func (r *RedigoDriver) UpdateTTLMany(prefix string /* prefix is the sid */, newSecondsLifeTime int64) error { - c := r.pool.Get() - defer c.Close() - if err := c.Err(); err != nil { - return err - } - - keys, err := r.getKeysConn(c, 0, prefix, true) - if err != nil { - return err - } - - for _, key := range keys { - if err = r.updateTTLConn(c, key, newSecondsLifeTime); err != nil { // fail on first error. - return err - } - } - - return err -} - -// GetAll returns all redis entries using the "SCAN" command (2.8+). -func (r *RedigoDriver) GetAll() (interface{}, error) { - c := r.pool.Get() - defer c.Close() - if err := c.Err(); err != nil { - return nil, err - } - - redisVal, err := c.Do("SCAN", 0) // 0 -> cursor - if err != nil { - return nil, err - } - - if redisVal == nil { - return nil, err - } - - return redisVal, nil -} - -func (r *RedigoDriver) getKeysConn(c redis.Conn, cursor interface{}, prefix string, includeSID bool) ([]string, error) { - if !includeSID { - prefix += r.Config.Delim // delim can be used for fast matching of only keys. - } - pattern := prefix + "*" - - if err := c.Send("SCAN", cursor, "MATCH", pattern, "COUNT", 300000); err != nil { - return nil, err - } - - if err := c.Flush(); err != nil { - return nil, err - } - - reply, err := c.Receive() - if err != nil || reply == nil { - return nil, err - } - - // it returns []interface, with two entries, the first one is the cursor, if "0" then full iteration - // and the second one is a slice of the keys as []interface{uint8....}. - - if replies, ok := reply.([]interface{}); ok { - if len(replies) == 2 { - // take the second, it must contain the slice of keys. - if keysSliceAsBytes, ok := replies[1].([]interface{}); ok { - n := len(keysSliceAsBytes) - if n <= 0 { - return nil, nil - } - - keys := make([]string, n) - - for i, k := range keysSliceAsBytes { - // key := fmt.Sprintf("%s", k)[len(r.Config.Prefix):] - // if key == prefix { - // continue // it's the session id itself. - // } - keys[i] = fmt.Sprintf("%s", k) - } - - if cur := fmt.Sprintf("%s", replies[0]); cur != "0" { - moreKeys, err := r.getKeysConn(c, cur, prefix, includeSID) - if err != nil { - return nil, err - } - - keys = append(keys, moreKeys...) - } - - return keys, nil - } - - } - } - - return nil, nil -} - -// GetKeys returns all redis keys using the "SCAN" with MATCH command. -// Read more at: https://redis.io/commands/scan#the-match-option. -func (r *RedigoDriver) GetKeys(prefix string) ([]string, error) { - c := r.pool.Get() - defer c.Close() - if err := c.Err(); err != nil { - return nil, err - } - - return r.getKeysConn(c, 0, prefix, false) -} - -// GetBytes returns value, err by its key -// you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{}) -// returns nil and a filled error if something wrong happens -func (r *RedigoDriver) GetBytes(key string) ([]byte, error) { - c := r.pool.Get() - defer c.Close() - if err := c.Err(); err != nil { - return nil, err - } - - redisVal, err := c.Do("GET", key) - if err != nil { - return nil, err - } - if redisVal == nil { - return nil, fmt.Errorf("%s: %w", key, ErrKeyNotFound) - } - - return redis.Bytes(redisVal, err) -} - -// Delete removes redis entry by specific key -func (r *RedigoDriver) Delete(key string) error { - c := r.pool.Get() - defer c.Close() - - _, err := c.Do("DEL", key) - return err -} - -// Connect connects to the redis, called only once. -func (r *RedigoDriver) Connect(c Config) error { - if c.Network == "" { - c.Network = DefaultRedisNetwork - } - - if c.Addr == "" { - c.Addr = DefaultRedisAddr - } - - pool := &redis.Pool{IdleTimeout: r.IdleTimeout, MaxIdle: r.MaxIdle, Wait: r.Wait, MaxActive: c.MaxActive} - pool.TestOnBorrow = func(c redis.Conn, t time.Time) error { - _, err := c.Do("PING") - return err - } - - var options []redis.DialOption - - if c.Timeout > 0 { - options = append(options, - redis.DialConnectTimeout(c.Timeout), - redis.DialReadTimeout(c.Timeout), - redis.DialWriteTimeout(c.Timeout)) - } - - if c.TLSConfig != nil { - options = append(options, - redis.DialTLSConfig(c.TLSConfig), - redis.DialUseTLS(true), - ) - } - - pool.Dial = func() (redis.Conn, error) { - conn, err := redis.Dial(c.Network, c.Addr, options...) - if err != nil { - return nil, err - } - - if c.Password != "" { - if _, err = conn.Do("AUTH", c.Password); err != nil { - conn.Close() - return nil, err - } - } - - if c.Database != "" { - if _, err = conn.Do("SELECT", c.Database); err != nil { - conn.Close() - return nil, err - } - } - - return conn, err - } - - r.Connected = true - r.pool = pool - r.Config = c - return nil -} diff --git a/sessions/sessions.go b/sessions/sessions.go index a98ad803..e8953c0c 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -5,6 +5,7 @@ import ( "time" "github.com/kataras/iris/v12/context" + "github.com/kataras/iris/v12/core/host" ) func init() { @@ -51,6 +52,9 @@ func New(cfg Config) *Sessions { // a session db doesn't have write access func (s *Sessions) UseDatabase(db Database) { db.SetLogger(s.config.Logger) // inject the logger. + host.RegisterOnInterrupt(func() { + db.Close() + }) s.provider.RegisterDatabase(db) } @@ -125,8 +129,6 @@ func (s *Sessions) Start(ctx *context.Context, cookieOptions ...context.CookieOp // fmt.Printf("%s=%s\n", key, value) // }) // } - sess.isNew = s.provider.db.Len(sid) == 0 - s.updateCookie(ctx, sid, s.config.Expires, cookieOptions...) return sess