From fb1a688007be0fedf912101f85a7c7df2fee56d1 Mon Sep 17 00:00:00 2001 From: WuYongXian Date: Wed, 2 Sep 2020 20:23:32 +0800 Subject: [PATCH 1/4] support session storage as hash --- sessions/sessiondb/redis_hash/database.go | 244 ++++++++++++++++++ sessions/sessiondb/redis_hash/driver.go | 26 ++ sessions/sessiondb/redis_hash/driver_radix.go | 208 +++++++++++++++ 3 files changed, 478 insertions(+) create mode 100644 sessions/sessiondb/redis_hash/database.go create mode 100644 sessions/sessiondb/redis_hash/driver.go create mode 100644 sessions/sessiondb/redis_hash/driver_radix.go diff --git a/sessions/sessiondb/redis_hash/database.go b/sessions/sessiondb/redis_hash/database.go new file mode 100644 index 0000000000..4e97242041 --- /dev/null +++ b/sessions/sessiondb/redis_hash/database.go @@ -0,0 +1,244 @@ +package redis_hash + +import ( + "time" + + "github.com/kataras/golog" + "github.com/kataras/iris/v12/sessions" +) + +const ( + // DefaultRedisNetwork the redis network option, "tcp". + DefaultRedisNetwork = "tcp" + // DefaultRedisAddr the redis address option, "127.0.0.1:6379". + DefaultRedisAddr = "127.0.0.1:6379" + // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second + DefaultRedisTimeout = time.Duration(30) * time.Second +) + +// Config the redis configuration used inside sessions +type Config struct { + // Network protocol. Defaults to "tcp". + Network string + // Addr of a single redis server instance. + // See "Clusters" field for clusters support. + // 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. + Clusters []string + // Password string .If no password then no 'AUTH'. Defaults to "". + Password string + // If Database is empty "" then no 'SELECT'. Defaults to "". + Database string + // MaxActive. Defaults to 10. + 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 + + // Driver only supports `Radix()` go clients for redis. + // Configure each driver by the return value of their constructors. + // + // Defaults to `Radix()`. + Driver Driver +} + +// DefaultConfig returns the default configuration for Redis service. +func DefaultConfig() Config { + return Config{ + Network: DefaultRedisNetwork, + Addr: DefaultRedisAddr, + Password: "", + Database: "", + MaxActive: 10, + Timeout: DefaultRedisTimeout, + Prefix: "", + Driver: Radix(), + } +} + +// Database the redis back-end session database for the sessions. +type Database struct { + c Config + logger *golog.Logger +} + +var _ sessions.Database = (*Database)(nil) + +// New returns a new redis database. +func New(cfg ...Config) *Database { + c := DefaultConfig() + if len(cfg) > 0 { + c = cfg[0] + + 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.Driver == nil { + c.Driver = Radix() + } + } + + if err := c.Driver.Connect(c); err != nil { + panic(err) + } + + db := &Database{c: c} + _, err := db.c.Driver.PingPong() + if err != nil { + panic(err) + } + + 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 +} + +// 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 { + seconds, hasExpiration, found := db.c.Driver.TTL(sid) + 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. + valueBytes, _ := sessions.DefaultTranscoder.Marshal(sid) + if err := db.c.Driver.Set(sid, sid, valueBytes, int64(expires.Seconds())); err != nil { + golog.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)} +} + +// 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(sid, int64(newExpires.Seconds())) +} + +// HSET key field value +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.Error(err) + return + } + + //fmt.Println("database.Set ", sid, "--", key, "---", int64(lifetime.DurationUntilExpiration().Seconds())) + // fmt.Printf("lifetime.DurationUntilExpiration(): %s. Seconds: %v\n", lifetime.DurationUntilExpiration(), lifetime.DurationUntilExpiration().Seconds()) + if err = db.c.Driver.Set(sid, key, valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { + golog.Debug(err) + } +} + +// HGET key field +func (db *Database) Get(sid string, key string) (value interface{}) { + db.get(sid, key, &value) + return +} + +func (db *Database) get(sid, key string, outPtr interface{}) { + data, err := db.c.Driver.Get(sid, key) + if err != nil { + // not found. + return + } + + if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil { + db.logger.Debugf("unable to unmarshal value of key: '%s': %v", key, err) + } +} + +func (db *Database) keys(sid string) []string { + keys, err := db.c.Driver.GetKeys(sid) + if err != nil { + db.logger.Debugf("unable to get all redis keys of session '%s': %v", sid, err) + return nil + } + + return keys +} + +// 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(sid, key, &value) + cb(key, value) + } +} + +// Len returns the length of the session's entries (keys). +func (db *Database) Len(sid string) (n int) { + length, err := db.c.Driver.Len(sid) + if err != nil { + db.logger.Debugf("get hash key length of session '%s': %v", sid, err) + return 0 + } + return length +} + +// HDEL key field1 [field2] +func (db *Database) Delete(sid string, key string) (deleted bool) { + err := db.c.Driver.Delete(sid, key) + if err != nil { + db.logger.Error(err) + } + return err == nil +} + +// DEL key +func (db *Database) Clear(sid string) { + err := db.c.Driver.Clear(sid) + if err != nil { + db.logger.Debugf("unable to delete session '%s': %v", sid, 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) { + // clear all session. + db.Clear(sid) +} + +// Close terminates the redis connection. +func (db *Database) Close() error { + return closeDB(db) +} + +func closeDB(db *Database) error { + return db.c.Driver.CloseConnection() +} diff --git a/sessions/sessiondb/redis_hash/driver.go b/sessions/sessiondb/redis_hash/driver.go new file mode 100644 index 0000000000..2952d4b3bc --- /dev/null +++ b/sessions/sessiondb/redis_hash/driver.go @@ -0,0 +1,26 @@ +package redis_hash + +// 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, field string, value interface{}, secondsLifetime int64) error + Get(key, field string) (interface{}, error) + TTL(key string) (seconds int64, hasExpiration bool, found bool) + UpdateTTL(key string, newSecondsLifeTime int64) error + UpdateTTLMany(key string, newSecondsLifeTime int64) error + GetKeys(key string) ([]string, error) + Delete(key, field string) error + Clear(key string) error + Len(key string) (int, error) +} + +var ( + _ Driver = (*RadixDriver)(nil) +) + +func Radix() *RadixDriver { + return &RadixDriver{} +} diff --git a/sessions/sessiondb/redis_hash/driver_radix.go b/sessions/sessiondb/redis_hash/driver_radix.go new file mode 100644 index 0000000000..300b933d3e --- /dev/null +++ b/sessions/sessiondb/redis_hash/driver_radix.go @@ -0,0 +1,208 @@ +package redis_hash + +import ( + "errors" + "fmt" + "github.com/mediocregopher/radix/v3" +) + +// RadixDriver the Redis service based on the radix go client, +// contains the config and the redis cluster. +type RadixDriver struct { + Connected bool //Connected is true when the Service has already connected + Config Config //Config the read-only redis database config. + pool radixPool +} + +type radixPool interface { + Do(a radix.Action) error + Close() error +} + +// 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 + } + + var options []radix.DialOpt + if c.Password != "" { + options = append(options, radix.DialAuthPass(c.Password)) + } + if c.Timeout > 0 { + options = append(options, radix.DialTimeout(c.Timeout)) + } + + var pool radixPool + var connFunc radix.ConnFunc + connFunc = func(network, addr string) (radix.Conn, error) { + return radix.Dial(network, addr, options...) + } + 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 errors.New("redis: already closed") +} + +// Using the "HSET key field value" command. +// The expiration is setted by the secondsLifetime. +func (r *RadixDriver) Set(key, field string, value interface{}, secondsLifetime int64) error { + var cmd radix.CmdAction + + cmd = radix.FlatCmd(nil, "HMSET", r.Config.Prefix+key, field, value) + err := r.pool.Do(cmd) + if err != nil { + return err + } + if secondsLifetime > 0 { + cmd = radix.FlatCmd(nil, "EXPIRE", r.Config.Prefix+key, secondsLifetime) + return r.pool.Do(cmd) + } + + return nil +} + +// Using the "HGET key field" command. +// returns nil and a filled error if something bad happened. +func (r *RadixDriver) Get(key, field string) (interface{}, error) { + var redisVal interface{} + mn := radix.MaybeNil{Rcv: &redisVal} + + err := r.pool.Do(radix.Cmd(&mn, "HGET", r.Config.Prefix+key, field)) + if err != nil { + return nil, err + } + if mn.Nil { + return nil, fmt.Errorf("%s %s: %w", r.Config.Prefix+key, field, errors.New("key not found")) + } + + 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", r.Config.Prefix+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, newSecondsLifeTime int64) error { + var reply int + err := r.pool.Do(radix.FlatCmd(&reply, "EXPIRE", r.Config.Prefix+key, newSecondsLifeTime)) + if err != nil { + return err + } + + // 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 +} + +// Using the "HKEYS key" command. +func (r *RadixDriver) getKeys(key string) ([]string, error) { + var res []string + err := r.pool.Do(radix.FlatCmd(&res, "HKEYS", r.Config.Prefix+key)) + if err != nil { + return nil, err + } + + return res, nil +} + +// Using the "EXPIRE" command. +func (r *RadixDriver) UpdateTTL(key string, newSecondsLifeTime int64) error { + return r.updateTTLConn(key, newSecondsLifeTime) +} + +// UpdateTTLMany like `UpdateTTL` all keys. +// look the `sessions/Database#OnUpdateExpiration` for example. +func (r *RadixDriver) UpdateTTLMany(key string, newSecondsLifeTime int64) error { + return r.updateTTLConn(key, newSecondsLifeTime) +} + +// GetKeys returns all redis hash keys using the "HKEYS key" with MATCH command. +func (r *RadixDriver) GetKeys(key string) ([]string, error) { + return r.getKeys(key) +} + +// Using the "HDEL key field1" command. +// Delete removes redis entry by specific key +func (r *RadixDriver) Delete(key, field string) error { + return r.pool.Do(radix.Cmd(nil, "HDEL", r.Config.Prefix+key, field)) +} + +// Using the "DEL key" command. +func (r *RadixDriver) Clear(key string) error { + return r.pool.Do(radix.Cmd(nil, "DEL", r.Config.Prefix+key)) +} + +// Using the "HLEN key" command. +func (r *RadixDriver) Len(key string) (int, error) { + var length int + err := r.pool.Do(radix.FlatCmd(&length, "HLEN", r.Config.Prefix+key)) + if err != nil { + return 0, err + } + return length, nil +} From 32c41569abbdb7739a6db1fb208d02bd10db8d89 Mon Sep 17 00:00:00 2001 From: WuYongXian Date: Wed, 2 Sep 2020 20:27:58 +0800 Subject: [PATCH 2/4] support session storage as hash --- sessions/sessiondb/redis_hash/database.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sessions/sessiondb/redis_hash/database.go b/sessions/sessiondb/redis_hash/database.go index 4e97242041..5d8c9e1d49 100644 --- a/sessions/sessiondb/redis_hash/database.go +++ b/sessions/sessiondb/redis_hash/database.go @@ -128,7 +128,7 @@ func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. valueBytes, _ := sessions.DefaultTranscoder.Marshal(sid) if err := db.c.Driver.Set(sid, sid, valueBytes, int64(expires.Seconds())); err != nil { - golog.Debug(err) + db.logger.Debug(err) } return sessions.LifeTime{} // session manager will handle the rest. @@ -151,14 +151,14 @@ func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) err 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.Error(err) + db.logger.Error(err) return } //fmt.Println("database.Set ", sid, "--", key, "---", int64(lifetime.DurationUntilExpiration().Seconds())) // fmt.Printf("lifetime.DurationUntilExpiration(): %s. Seconds: %v\n", lifetime.DurationUntilExpiration(), lifetime.DurationUntilExpiration().Seconds()) if err = db.c.Driver.Set(sid, key, valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { - golog.Debug(err) + db.logger.Debug(err) } } From 918c521c4f4bec6f2f35151294f1af47fb7296a5 Mon Sep 17 00:00:00 2001 From: WuYongXian Date: Wed, 2 Sep 2020 20:49:16 +0800 Subject: [PATCH 3/4] support session storage as hash --- sessions/sessiondb/{redis_hash => redishash}/database.go | 2 +- sessions/sessiondb/{redis_hash => redishash}/driver.go | 2 +- sessions/sessiondb/{redis_hash => redishash}/driver_radix.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename sessions/sessiondb/{redis_hash => redishash}/database.go (99%) rename sessions/sessiondb/{redis_hash => redishash}/driver.go (97%) rename sessions/sessiondb/{redis_hash => redishash}/driver_radix.go (99%) diff --git a/sessions/sessiondb/redis_hash/database.go b/sessions/sessiondb/redishash/database.go similarity index 99% rename from sessions/sessiondb/redis_hash/database.go rename to sessions/sessiondb/redishash/database.go index 5d8c9e1d49..6b89b8d4a4 100644 --- a/sessions/sessiondb/redis_hash/database.go +++ b/sessions/sessiondb/redishash/database.go @@ -1,4 +1,4 @@ -package redis_hash +package redishash import ( "time" diff --git a/sessions/sessiondb/redis_hash/driver.go b/sessions/sessiondb/redishash/driver.go similarity index 97% rename from sessions/sessiondb/redis_hash/driver.go rename to sessions/sessiondb/redishash/driver.go index 2952d4b3bc..eb27232c30 100644 --- a/sessions/sessiondb/redis_hash/driver.go +++ b/sessions/sessiondb/redishash/driver.go @@ -1,4 +1,4 @@ -package redis_hash +package redishash // Driver is the interface which each supported redis client // should support in order to be used in the redis session database. diff --git a/sessions/sessiondb/redis_hash/driver_radix.go b/sessions/sessiondb/redishash/driver_radix.go similarity index 99% rename from sessions/sessiondb/redis_hash/driver_radix.go rename to sessions/sessiondb/redishash/driver_radix.go index 300b933d3e..c2294709b8 100644 --- a/sessions/sessiondb/redis_hash/driver_radix.go +++ b/sessions/sessiondb/redishash/driver_radix.go @@ -1,4 +1,4 @@ -package redis_hash +package redishash import ( "errors" From 45a104d69bfe749908ab21797119af5a61c1312e Mon Sep 17 00:00:00 2001 From: WuYongXian Date: Thu, 3 Sep 2020 15:47:27 +0800 Subject: [PATCH 4/4] support session storage as hash --- sessions/sessiondb/redis/database.go | 133 ++-------- sessions/sessiondb/redis/database_driver.go | 28 ++ .../sessiondb/redis/database_driver_hashed.go | 157 +++++++++++ .../sessiondb/redis/database_driver_string.go | 165 ++++++++++++ sessions/sessiondb/redis/driver.go | 15 +- sessions/sessiondb/redis/driver_radix.go | 41 ++- .../driver_radix_hashed.go} | 57 ++-- sessions/sessiondb/redis/driver_redigo.go | 39 ++- sessions/sessiondb/redishash/database.go | 244 ------------------ sessions/sessiondb/redishash/driver.go | 26 -- 10 files changed, 491 insertions(+), 414 deletions(-) create mode 100644 sessions/sessiondb/redis/database_driver.go create mode 100644 sessions/sessiondb/redis/database_driver_hashed.go create mode 100644 sessions/sessiondb/redis/database_driver_string.go rename sessions/sessiondb/{redishash/driver_radix.go => redis/driver_radix_hashed.go} (75%) delete mode 100644 sessions/sessiondb/redishash/database.go delete mode 100644 sessions/sessiondb/redishash/driver.go diff --git a/sessions/sessiondb/redis/database.go b/sessions/sessiondb/redis/database.go index 2b1898d609..c32018782d 100644 --- a/sessions/sessiondb/redis/database.go +++ b/sessions/sessiondb/redis/database.go @@ -3,7 +3,6 @@ package redis import ( "crypto/tls" "errors" - "strings" "time" "github.com/kataras/iris/v12/sessions" @@ -77,8 +76,7 @@ func DefaultConfig() Config { // Database the redis back-end session database for the sessions. type Database struct { - c Config - logger *golog.Logger + driver DatabaseDriver } var _ sessions.Database = (*Database)(nil) @@ -86,6 +84,7 @@ var _ sessions.Database = (*Database)(nil) // New returns a new redis database. func New(cfg ...Config) *Database { c := DefaultConfig() + var driver DatabaseDriver if len(cfg) > 0 { c = cfg[0] @@ -113,167 +112,81 @@ func New(cfg ...Config) *Database { c.Driver = Redigo() } } - - if err := c.Driver.Connect(c); err != nil { - panic(err) + if _, ok := c.Driver.(*RadixDriverHashed); ok { + driver = DatabaseDriverHashed(c) + } else { + driver = DatabaseDriverString(c) } - - db := &Database{c: c} - _, err := db.c.Driver.PingPong() - if err != nil { + if err := driver.Connect(c); err != nil { panic(err) } - // runtime.SetFinalizer(db, closeDB) + + db := &Database{driver: driver} 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. + return db.driver.Config() // 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 + db.driver.SetLogger(logger) } // 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 { - 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)} + return db.driver.Acquire(sid, expires) } // 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())) + return db.driver.OnUpdateExpiration(sid, newExpires) } -func (db *Database) makeKey(sid, key string) string { - if key == "" { - return db.c.Prefix + sid - } - return db.c.Prefix + sid + db.c.Delim + 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 { - 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) - } + db.driver.Set(sid, lifetime, key, value, immutable) + return } // 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) - return err - } - - return nil -} - -func (db *Database) keys(sid string) []string { - keys, err := db.c.Driver.GetKeys(db.makeKey(sid, "")) - if err != nil { - db.logger.Debugf("unable to get all redis keys of session '%s': %v", sid, err) - return nil - } - - return keys + return db.driver.Get(sid, key) } // 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) - } + db.driver.Visit(sid, cb) + return } // Len returns the length of the session's entries (keys). func (db *Database) Len(sid string) (n int) { - return len(db.keys(sid)) + return db.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)) - if err != nil { - db.logger.Error(err) - } - return err == nil + return db.driver.Delete(sid, key) } // Clear removes all session key values but it keeps the session entry. func (db *Database) Clear(sid string) { - keys := db.keys(sid) - for _, key := range keys { - if err := db.c.Driver.Delete(key); err != nil { - db.logger.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err) - } - } + db.driver.Clear(sid) } // 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) - if err != nil { - db.logger.Debugf("Database.Release.Driver.Delete: %s: %v", sid, err) - } + db.driver.Release(sid) } // Close terminates the redis connection. func (db *Database) Close() error { - return closeDB(db) -} - -func closeDB(db *Database) error { - return db.c.Driver.CloseConnection() + return db.driver.Close() } var ( diff --git a/sessions/sessiondb/redis/database_driver.go b/sessions/sessiondb/redis/database_driver.go new file mode 100644 index 0000000000..0aa89558ae --- /dev/null +++ b/sessions/sessiondb/redis/database_driver.go @@ -0,0 +1,28 @@ +package redis + +import ( + "github.com/kataras/iris/v12/sessions" +) + +type DatabaseDriver interface { + sessions.Database + + Config() *Config + + Connect(c Config) error + + Close() error +} + +var ( + _ DatabaseDriver = (*DatabaseString)(nil) + _ DatabaseDriver = (*DatabaseHashed)(nil) +) + +func DatabaseDriverString(conf Config) *DatabaseString { + return &DatabaseString{c: conf} +} + +func DatabaseDriverHashed(conf Config) *DatabaseHashed { + return &DatabaseHashed{c: conf} +} diff --git a/sessions/sessiondb/redis/database_driver_hashed.go b/sessions/sessiondb/redis/database_driver_hashed.go new file mode 100644 index 0000000000..3b4f380fc8 --- /dev/null +++ b/sessions/sessiondb/redis/database_driver_hashed.go @@ -0,0 +1,157 @@ +package redis + +import ( + "github.com/kataras/golog" + "github.com/kataras/iris/v12/sessions" + "time" +) + +type DatabaseHashed struct { + c Config + logger *golog.Logger +} + +// Config returns the configuration for the redis server bridge, you can change them. +func (db *DatabaseHashed) Config() *Config { + return &db.c // 6 Aug 2019 - keep that for no breaking change. +} + +func (db *DatabaseHashed) SetLogger(logger *golog.Logger) { + db.logger = logger +} + +func (db *DatabaseHashed) Connect(c Config) error { + err := db.c.Driver.Connect(c) + if err != nil { + return err + } + + _, err = db.c.Driver.PingPong() + return err +} + +func (db *DatabaseHashed) makeKey(sid, key string) string { + if key == "" { + return db.c.Prefix + sid + } + return db.c.Prefix + sid + db.c.Delim + key +} + +// 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 *DatabaseHashed) Acquire(sid string, expires time.Duration) sessions.LifeTime { + seconds, hasExpiration, found := db.c.Driver.TTL(sid) + 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. + valueBytes, _ := sessions.DefaultTranscoder.Marshal(sid) + if err := db.c.Driver.Set(sid, sid, valueBytes, int64(expires.Seconds())); 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)} +} + +// OnUpdateExpiration will re-set the database's session's entry ttl. +// https://redis.io/commands/expire#refreshing-expires +func (db *DatabaseHashed) OnUpdateExpiration(sid string, newExpires time.Duration) error { + return db.c.Driver.UpdateTTLMany(sid, int64(newExpires.Seconds())) +} + +// Set sets a key value of a specific session. +// Ignore the "immutable". +func (db *DatabaseHashed) Set(sid string, lifetime *sessions.LifeTime, key string, value interface{}, immutable bool) { + valueBytes, err := sessions.DefaultTranscoder.Marshal(value) + if err != nil { + db.logger.Error(err) + return + } + + //fmt.Println("database.Set ", sid, "--", key, "---", int64(lifetime.DurationUntilExpiration().Seconds())) + // fmt.Printf("lifetime.DurationUntilExpiration(): %s. Seconds: %v\n", lifetime.DurationUntilExpiration(), lifetime.DurationUntilExpiration().Seconds()) + if err = db.c.Driver.Set(sid, key, valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { + db.logger.Debug(err) + } +} + +func (db *DatabaseHashed) get(sid, key string, outPtr interface{}) { + data, err := db.c.Driver.Get(sid, key) + if err != nil { + // not found. + return + } + + if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil { + db.logger.Debugf("unable to unmarshal value of key: '%s': %v", key, err) + } +} + +func (db *DatabaseHashed) keys(sid string) []string { + keys, err := db.c.Driver.GetKeys(sid) + if err != nil { + db.logger.Debugf("unable to get all redis keys of session '%s': %v", sid, err) + return nil + } + + return keys +} + +// Get retrieves a session value based on the key. +func (db *DatabaseHashed) Get(sid string, key string) (value interface{}) { + db.get(sid, key, &value) + return +} + +// Visit loops through all session keys and values. +func (db *DatabaseHashed) 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(sid, key, &value) + cb(key, value) + } +} + +// Len returns the length of the session's entries (keys). +func (db *DatabaseHashed) Len(sid string) (n int) { + length, err := db.c.Driver.Len(sid) + if err != nil { + db.logger.Debugf("get hash key length of session '%s': %v", sid, err) + return 0 + } + return length +} + +// Delete removes a session key value based on its key. +func (db *DatabaseHashed) Delete(sid string, key string) (deleted bool) { + err := db.c.Driver.Delete(sid, key) + if err != nil { + db.logger.Error(err) + } + return err == nil +} + +// Clear removes all session key values but it keeps the session entry. +func (db *DatabaseHashed) Clear(sid string) { + err := db.c.Driver.Clear(sid) + if err != nil { + db.logger.Debugf("unable to delete session '%s': %v", sid, 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 *DatabaseHashed) Release(sid string) { + db.Clear(sid) +} + +func (db *DatabaseHashed) Close() error { + return db.c.Driver.CloseConnection() +} diff --git a/sessions/sessiondb/redis/database_driver_string.go b/sessions/sessiondb/redis/database_driver_string.go new file mode 100644 index 0000000000..e19767470e --- /dev/null +++ b/sessions/sessiondb/redis/database_driver_string.go @@ -0,0 +1,165 @@ +package redis + +import ( + "github.com/kataras/golog" + "github.com/kataras/iris/v12/sessions" + "strings" + "time" +) + +type DatabaseString struct { + c Config + logger *golog.Logger +} + +// Config returns the configuration for the redis server bridge, you can change them. +func (db *DatabaseString) Config() *Config { + return &db.c // 6 Aug 2019 - keep that for no breaking change. +} + +func (db *DatabaseString) SetLogger(logger *golog.Logger) { + db.logger = logger +} + +func (db *DatabaseString) Connect(c Config) error { + err := db.c.Driver.Connect(c) + if err != nil { + return err + } + + _, err = db.c.Driver.PingPong() + return err +} + +func MakeKey(sid, key, prefix, delim string) string { + if key == "" { + return prefix + sid + } + return prefix + sid + delim + key +} + +// 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 *DatabaseString) Acquire(sid string, expires time.Duration) sessions.LifeTime { + key := MakeKey(sid, "", db.c.Prefix, db.c.Delim) + 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(sid, "", sid, int64(expires.Seconds())); 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)} +} + +// OnUpdateExpiration will re-set the database's session's entry ttl. +// https://redis.io/commands/expire#refreshing-expires +func (db *DatabaseString) OnUpdateExpiration(sid string, newExpires time.Duration) error { + return db.c.Driver.UpdateTTLMany(MakeKey(sid, "", db.c.Prefix, db.c.Delim), int64(newExpires.Seconds())) +} + +// Set sets a key value of a specific session. +// Ignore the "immutable". +func (db *DatabaseString) Set(sid string, lifetime *sessions.LifeTime, key string, value interface{}, immutable bool) { + 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(sid, key, valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { + db.logger.Debug(err) + } +} + +func (db *DatabaseString) get(key string, outPtr interface{}) error { + data, err := db.c.Driver.GetByKey(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) + return err + } + + return nil +} + +func (db *DatabaseString) keys(sid string) []string { + keys, err := db.c.Driver.GetKeys(MakeKey(sid, "", db.c.Prefix, db.c.Delim)) + if err != nil { + db.logger.Debugf("unable to get all redis keys of session '%s': %v", sid, err) + return nil + } + + return keys +} + +// Get retrieves a session value based on the key. +func (db *DatabaseString) Get(sid string, key string) (value interface{}) { + db.get(MakeKey(sid, key, db.c.Prefix, db.c.Delim), &value) + return +} + +// Visit loops through all session keys and values. +func (db *DatabaseString) 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) + } +} + +// Len returns the length of the session's entries (keys). +func (db *DatabaseString) Len(sid string) (n int) { + return len(db.keys(sid)) +} + +// Delete removes a session key value based on its key. +func (db *DatabaseString) Delete(sid string, key string) (deleted bool) { + err := db.c.Driver.DeleteByKey(MakeKey(sid, key, db.c.Prefix, db.c.Delim)) + if err != nil { + db.logger.Error(err) + } + return err == nil +} + +// Clear removes all session key values but it keeps the session entry. +func (db *DatabaseString) Clear(sid string) { + keys := db.keys(sid) + for _, key := range keys { + if err := db.c.Driver.DeleteByKey(key); err != nil { + db.logger.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, 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 *DatabaseString) Release(sid string) { + // clear all $sid-$key. + db.Clear(sid) + // and remove the $sid. + err := db.c.Driver.DeleteByKey(db.c.Prefix + sid) + if err != nil { + db.logger.Debugf("Database.Release.Driver.Delete: %s: %v", sid, err) + } +} + +func (db *DatabaseString) Close() error { + return db.c.Driver.CloseConnection() +} diff --git a/sessions/sessiondb/redis/driver.go b/sessions/sessiondb/redis/driver.go index 3aae3ab9c5..f935a4184e 100644 --- a/sessions/sessiondb/redis/driver.go +++ b/sessions/sessiondb/redis/driver.go @@ -6,19 +6,24 @@ 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) + Set(key, field string, value interface{}, secondsLifetime int64) error + Get(key, field string) (interface{}, error) + GetByKey(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 + DeleteByKey(key string) error + Delete(key, field string) error + Clear(key string) error + Len(key string) (int, error) } var ( _ Driver = (*RedigoDriver)(nil) _ Driver = (*RadixDriver)(nil) + _ Driver = (*RadixDriverHashed)(nil) ) // Redigo returns the driver for the redigo go redis client. @@ -32,3 +37,7 @@ func Redigo() *RedigoDriver { func Radix() *RadixDriver { return &RadixDriver{} } + +func RadixHashed() *RadixDriverHashed { + return &RadixDriverHashed{} +} diff --git a/sessions/sessiondb/redis/driver_radix.go b/sessions/sessiondb/redis/driver_radix.go index 663ad08617..3791e7e026 100644 --- a/sessions/sessiondb/redis/driver_radix.go +++ b/sessions/sessiondb/redis/driver_radix.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "github.com/kataras/golog" "strconv" "github.com/mediocregopher/radix/v3" @@ -97,7 +98,7 @@ func (r *RadixDriver) Connect(c Config) error { } else { */ connFunc = func(network, addr string) (radix.Conn, error) { - return radix.Dial(c.Network, c.Addr, options...) + return radix.Dial(network, addr, options...) } var pool radixPool @@ -148,7 +149,8 @@ func (r *RadixDriver) CloseConnection() error { // 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 { +func (r *RadixDriver) Set(sid, field string, value interface{}, secondsLifetime int64) error { + key := MakeKey(sid, field, r.Config.Prefix, r.Config.Delim) var cmd radix.CmdAction // if has expiration, then use the "EX" to delete the key automatically. if secondsLifetime > 0 { @@ -162,7 +164,12 @@ func (r *RadixDriver) Set(key string, value interface{}, secondsLifetime int64) // 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) { +func (r *RadixDriver) Get(sid, field string /* full key */) (interface{}, error) { + key := MakeKey(sid, field, r.Config.Prefix, r.Config.Delim) + return r.GetByKey(key) +} + +func (r *RadixDriver) GetByKey(key string) (interface{}, error) { var redisVal interface{} mn := radix.MaybeNil{Rcv: &redisVal} @@ -315,7 +322,33 @@ func (r *RadixDriver) GetKeys(prefix string) ([]string, error) { } // Delete removes redis entry by specific key -func (r *RadixDriver) Delete(key string) error { +func (r *RadixDriver) DeleteByKey(key string) error { err := r.pool.Do(radix.Cmd(nil, "DEL", key)) return err } + +func (r *RadixDriver) Delete(sid, field string) error { + key := MakeKey(sid, field, r.Config.Prefix, r.Config.Delim) + return r.DeleteByKey(key) +} + +func (r *RadixDriver) Clear(sid string) error { + keys, err := r.GetKeys(MakeKey(sid, "", r.Config.Prefix, r.Config.Delim)) + if err != nil { + return err + } + for _, key := range keys { + if err := r.DeleteByKey(key); err != nil { + golog.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err) + } + } + return nil +} + +func (r *RadixDriver) Len(sid string) (int, error) { + keys, err := r.GetKeys(MakeKey(sid, "", r.Config.Prefix, r.Config.Delim)) + if err != nil { + return 0, err + } + return len(keys), nil +} diff --git a/sessions/sessiondb/redishash/driver_radix.go b/sessions/sessiondb/redis/driver_radix_hashed.go similarity index 75% rename from sessions/sessiondb/redishash/driver_radix.go rename to sessions/sessiondb/redis/driver_radix_hashed.go index c2294709b8..0889357a3b 100644 --- a/sessions/sessiondb/redishash/driver_radix.go +++ b/sessions/sessiondb/redis/driver_radix_hashed.go @@ -1,26 +1,20 @@ -package redishash +package redis import ( - "errors" "fmt" "github.com/mediocregopher/radix/v3" ) // RadixDriver the Redis service based on the radix go client, // contains the config and the redis cluster. -type RadixDriver struct { +type RadixDriverHashed struct { Connected bool //Connected is true when the Service has already connected Config Config //Config the read-only redis database config. pool radixPool } -type radixPool interface { - Do(a radix.Action) error - Close() error -} - // Connect connects to the redis, called only once -func (r *RadixDriver) Connect(c Config) error { +func (r *RadixDriverHashed) Connect(c Config) error { if c.Timeout < 0 { c.Timeout = DefaultRedisTimeout } @@ -41,6 +35,9 @@ func (r *RadixDriver) Connect(c Config) error { if c.Timeout > 0 { options = append(options, radix.DialTimeout(c.Timeout)) } + if c.TLSConfig != nil { + options = append(options, radix.DialUseTLS(c.TLSConfig)) + } var pool radixPool var connFunc radix.ConnFunc @@ -71,7 +68,7 @@ func (r *RadixDriver) Connect(c Config) error { } // PingPong sends a ping and receives a pong, if no pong received then returns false and filled error -func (r *RadixDriver) PingPong() (bool, error) { +func (r *RadixDriverHashed) PingPong() (bool, error) { var msg string err := r.pool.Do(radix.Cmd(&msg, "PING")) if err != nil { @@ -82,17 +79,17 @@ func (r *RadixDriver) PingPong() (bool, error) { } // CloseConnection closes the redis connection. -func (r *RadixDriver) CloseConnection() error { +func (r *RadixDriverHashed) CloseConnection() error { if r.pool != nil { return r.pool.Close() } - return errors.New("redis: already closed") + return ErrRedisClosed } // Using the "HSET key field value" command. // The expiration is setted by the secondsLifetime. -func (r *RadixDriver) Set(key, field string, value interface{}, secondsLifetime int64) error { +func (r *RadixDriverHashed) Set(key, field string, value interface{}, secondsLifetime int64) error { var cmd radix.CmdAction cmd = radix.FlatCmd(nil, "HMSET", r.Config.Prefix+key, field, value) @@ -110,7 +107,7 @@ func (r *RadixDriver) Set(key, field string, value interface{}, secondsLifetime // Using the "HGET key field" command. // returns nil and a filled error if something bad happened. -func (r *RadixDriver) Get(key, field string) (interface{}, error) { +func (r *RadixDriverHashed) Get(key, field string) (interface{}, error) { var redisVal interface{} mn := radix.MaybeNil{Rcv: &redisVal} @@ -119,7 +116,7 @@ func (r *RadixDriver) Get(key, field string) (interface{}, error) { return nil, err } if mn.Nil { - return nil, fmt.Errorf("%s %s: %w", r.Config.Prefix+key, field, errors.New("key not found")) + return nil, fmt.Errorf("%s %s: %w", r.Config.Prefix+key, field, ErrKeyNotFound) } return redisVal, nil @@ -127,7 +124,7 @@ func (r *RadixDriver) Get(key, field string) (interface{}, error) { // 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) { +func (r *RadixDriverHashed) TTL(key string) (seconds int64, hasExpiration bool, found bool) { var redisVal interface{} err := r.pool.Do(radix.Cmd(&redisVal, "TTL", r.Config.Prefix+key)) if err != nil { @@ -141,7 +138,7 @@ func (r *RadixDriver) TTL(key string) (seconds int64, hasExpiration bool, found return } -func (r *RadixDriver) updateTTLConn(key string, newSecondsLifeTime int64) error { +func (r *RadixDriverHashed) updateTTLConn(key string, newSecondsLifeTime int64) error { var reply int err := r.pool.Do(radix.FlatCmd(&reply, "EXPIRE", r.Config.Prefix+key, newSecondsLifeTime)) if err != nil { @@ -160,7 +157,7 @@ func (r *RadixDriver) updateTTLConn(key string, newSecondsLifeTime int64) error } // Using the "HKEYS key" command. -func (r *RadixDriver) getKeys(key string) ([]string, error) { +func (r *RadixDriverHashed) getKeys(key string) ([]string, error) { var res []string err := r.pool.Do(radix.FlatCmd(&res, "HKEYS", r.Config.Prefix+key)) if err != nil { @@ -171,34 +168,34 @@ func (r *RadixDriver) getKeys(key string) ([]string, error) { } // Using the "EXPIRE" command. -func (r *RadixDriver) UpdateTTL(key string, newSecondsLifeTime int64) error { +func (r *RadixDriverHashed) UpdateTTL(key string, newSecondsLifeTime int64) error { return r.updateTTLConn(key, newSecondsLifeTime) } // UpdateTTLMany like `UpdateTTL` all keys. // look the `sessions/Database#OnUpdateExpiration` for example. -func (r *RadixDriver) UpdateTTLMany(key string, newSecondsLifeTime int64) error { +func (r *RadixDriverHashed) UpdateTTLMany(key string, newSecondsLifeTime int64) error { return r.updateTTLConn(key, newSecondsLifeTime) } // GetKeys returns all redis hash keys using the "HKEYS key" with MATCH command. -func (r *RadixDriver) GetKeys(key string) ([]string, error) { +func (r *RadixDriverHashed) GetKeys(key string) ([]string, error) { return r.getKeys(key) } // Using the "HDEL key field1" command. // Delete removes redis entry by specific key -func (r *RadixDriver) Delete(key, field string) error { +func (r *RadixDriverHashed) Delete(key, field string) error { return r.pool.Do(radix.Cmd(nil, "HDEL", r.Config.Prefix+key, field)) } // Using the "DEL key" command. -func (r *RadixDriver) Clear(key string) error { +func (r *RadixDriverHashed) Clear(key string) error { return r.pool.Do(radix.Cmd(nil, "DEL", r.Config.Prefix+key)) } // Using the "HLEN key" command. -func (r *RadixDriver) Len(key string) (int, error) { +func (r *RadixDriverHashed) Len(key string) (int, error) { var length int err := r.pool.Do(radix.FlatCmd(&length, "HLEN", r.Config.Prefix+key)) if err != nil { @@ -206,3 +203,15 @@ func (r *RadixDriver) Len(key string) (int, error) { } return length, nil } + +func (r *RadixDriverHashed) DeleteByKey(key string) error { + return nil +} + +func (r *RadixDriverHashed) GetAll() (interface{}, error) { + return nil, nil +} + +func (r *RadixDriverHashed) GetByKey(key string) (interface{}, error) { + return nil, nil +} diff --git a/sessions/sessiondb/redis/driver_redigo.go b/sessions/sessiondb/redis/driver_redigo.go index 0c3cbfdb7b..7b474d7af3 100644 --- a/sessions/sessiondb/redis/driver_redigo.go +++ b/sessions/sessiondb/redis/driver_redigo.go @@ -2,6 +2,7 @@ package redis import ( "fmt" + "github.com/kataras/golog" "time" "github.com/gomodule/redigo/redis" @@ -52,7 +53,8 @@ func (r *RedigoDriver) CloseConnection() error { // 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) { +func (r *RedigoDriver) Set(sid, field string, value interface{}, secondsLifetime int64) (err error) { + key := MakeKey(sid, field, r.Config.Prefix, r.Config.Delim) c := r.pool.Get() defer c.Close() if c.Err() != nil { @@ -71,7 +73,12 @@ func (r *RedigoDriver) Set(key string, value interface{}, secondsLifetime int64) // 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) { +func (r *RedigoDriver) Get(sid, field string) (interface{}, error) { + key := MakeKey(sid, field, r.Config.Prefix, r.Config.Delim) + return r.GetByKey(key) +} + +func (r *RedigoDriver) GetByKey(key string) (interface{}, error) { c := r.pool.Get() defer c.Close() if err := c.Err(); err != nil { @@ -277,7 +284,7 @@ func (r *RedigoDriver) GetBytes(key string) ([]byte, error) { } // Delete removes redis entry by specific key -func (r *RedigoDriver) Delete(key string) error { +func (r *RedigoDriver) DeleteByKey(key string) error { c := r.pool.Get() defer c.Close() @@ -345,3 +352,29 @@ func (r *RedigoDriver) Connect(c Config) error { r.Config = c return nil } + +func (r *RedigoDriver) Delete(sid, field string) error { + key := MakeKey(sid, field, r.Config.Prefix, r.Config.Delim) + return r.DeleteByKey(key) +} + +func (r *RedigoDriver) Clear(sid string) error { + keys, err := r.GetKeys(MakeKey(sid, "", r.Config.Prefix, r.Config.Delim)) + if err != nil { + return err + } + for _, key := range keys { + if err := r.DeleteByKey(key); err != nil { + golog.Debugf("unable to delete session '%s' value of key: '%s': %v", sid, key, err) + } + } + return nil +} + +func (r *RedigoDriver) Len(sid string) (int, error) { + keys, err := r.GetKeys(MakeKey(sid, "", r.Config.Prefix, r.Config.Delim)) + if err != nil { + return 0, err + } + return len(keys), nil +} diff --git a/sessions/sessiondb/redishash/database.go b/sessions/sessiondb/redishash/database.go deleted file mode 100644 index 6b89b8d4a4..0000000000 --- a/sessions/sessiondb/redishash/database.go +++ /dev/null @@ -1,244 +0,0 @@ -package redishash - -import ( - "time" - - "github.com/kataras/golog" - "github.com/kataras/iris/v12/sessions" -) - -const ( - // DefaultRedisNetwork the redis network option, "tcp". - DefaultRedisNetwork = "tcp" - // DefaultRedisAddr the redis address option, "127.0.0.1:6379". - DefaultRedisAddr = "127.0.0.1:6379" - // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second - DefaultRedisTimeout = time.Duration(30) * time.Second -) - -// Config the redis configuration used inside sessions -type Config struct { - // Network protocol. Defaults to "tcp". - Network string - // Addr of a single redis server instance. - // See "Clusters" field for clusters support. - // 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. - Clusters []string - // Password string .If no password then no 'AUTH'. Defaults to "". - Password string - // If Database is empty "" then no 'SELECT'. Defaults to "". - Database string - // MaxActive. Defaults to 10. - 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 - - // Driver only supports `Radix()` go clients for redis. - // Configure each driver by the return value of their constructors. - // - // Defaults to `Radix()`. - Driver Driver -} - -// DefaultConfig returns the default configuration for Redis service. -func DefaultConfig() Config { - return Config{ - Network: DefaultRedisNetwork, - Addr: DefaultRedisAddr, - Password: "", - Database: "", - MaxActive: 10, - Timeout: DefaultRedisTimeout, - Prefix: "", - Driver: Radix(), - } -} - -// Database the redis back-end session database for the sessions. -type Database struct { - c Config - logger *golog.Logger -} - -var _ sessions.Database = (*Database)(nil) - -// New returns a new redis database. -func New(cfg ...Config) *Database { - c := DefaultConfig() - if len(cfg) > 0 { - c = cfg[0] - - 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.Driver == nil { - c.Driver = Radix() - } - } - - if err := c.Driver.Connect(c); err != nil { - panic(err) - } - - db := &Database{c: c} - _, err := db.c.Driver.PingPong() - if err != nil { - panic(err) - } - - 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 -} - -// 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 { - seconds, hasExpiration, found := db.c.Driver.TTL(sid) - 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. - valueBytes, _ := sessions.DefaultTranscoder.Marshal(sid) - if err := db.c.Driver.Set(sid, sid, valueBytes, int64(expires.Seconds())); 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)} -} - -// 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(sid, int64(newExpires.Seconds())) -} - -// HSET key field value -func (db *Database) Set(sid string, lifetime *sessions.LifeTime, key string, value interface{}, immutable bool) { - valueBytes, err := sessions.DefaultTranscoder.Marshal(value) - if err != nil { - db.logger.Error(err) - return - } - - //fmt.Println("database.Set ", sid, "--", key, "---", int64(lifetime.DurationUntilExpiration().Seconds())) - // fmt.Printf("lifetime.DurationUntilExpiration(): %s. Seconds: %v\n", lifetime.DurationUntilExpiration(), lifetime.DurationUntilExpiration().Seconds()) - if err = db.c.Driver.Set(sid, key, valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())); err != nil { - db.logger.Debug(err) - } -} - -// HGET key field -func (db *Database) Get(sid string, key string) (value interface{}) { - db.get(sid, key, &value) - return -} - -func (db *Database) get(sid, key string, outPtr interface{}) { - data, err := db.c.Driver.Get(sid, key) - if err != nil { - // not found. - return - } - - if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil { - db.logger.Debugf("unable to unmarshal value of key: '%s': %v", key, err) - } -} - -func (db *Database) keys(sid string) []string { - keys, err := db.c.Driver.GetKeys(sid) - if err != nil { - db.logger.Debugf("unable to get all redis keys of session '%s': %v", sid, err) - return nil - } - - return keys -} - -// 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(sid, key, &value) - cb(key, value) - } -} - -// Len returns the length of the session's entries (keys). -func (db *Database) Len(sid string) (n int) { - length, err := db.c.Driver.Len(sid) - if err != nil { - db.logger.Debugf("get hash key length of session '%s': %v", sid, err) - return 0 - } - return length -} - -// HDEL key field1 [field2] -func (db *Database) Delete(sid string, key string) (deleted bool) { - err := db.c.Driver.Delete(sid, key) - if err != nil { - db.logger.Error(err) - } - return err == nil -} - -// DEL key -func (db *Database) Clear(sid string) { - err := db.c.Driver.Clear(sid) - if err != nil { - db.logger.Debugf("unable to delete session '%s': %v", sid, 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) { - // clear all session. - db.Clear(sid) -} - -// Close terminates the redis connection. -func (db *Database) Close() error { - return closeDB(db) -} - -func closeDB(db *Database) error { - return db.c.Driver.CloseConnection() -} diff --git a/sessions/sessiondb/redishash/driver.go b/sessions/sessiondb/redishash/driver.go deleted file mode 100644 index eb27232c30..0000000000 --- a/sessions/sessiondb/redishash/driver.go +++ /dev/null @@ -1,26 +0,0 @@ -package redishash - -// 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, field string, value interface{}, secondsLifetime int64) error - Get(key, field string) (interface{}, error) - TTL(key string) (seconds int64, hasExpiration bool, found bool) - UpdateTTL(key string, newSecondsLifeTime int64) error - UpdateTTLMany(key string, newSecondsLifeTime int64) error - GetKeys(key string) ([]string, error) - Delete(key, field string) error - Clear(key string) error - Len(key string) (int, error) -} - -var ( - _ Driver = (*RadixDriver)(nil) -) - -func Radix() *RadixDriver { - return &RadixDriver{} -}