fix #1610 #1651 - read HISTORY.md

This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-10-04 16:50:21 +03:00
parent 5fc50a0049
commit cc7e3860f2
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
16 changed files with 484 additions and 874 deletions

View File

@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and
## Fixes and Improvements ## 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. - 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 [#1650](https://github.com/kataras/iris/issues/1650)
- Fix [#1649](https://github.com/kataras/iris/issues/1649) - Fix [#1649](https://github.com/kataras/iris/issues/1649)

15
NOTICE
View File

@ -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 Package Version Website
@ -82,16 +82,13 @@ Revision ID: ab226d925aa394ccecf01e515ea8479367e0961c
b63c98b3c0 b63c98b3c0
protobuf 6c66de79d66478d https://github.com/golang/protobuf protobuf 6c66de79d66478d https://github.com/golang/protobuf
166c7ea05f5d2cc 166c7ea05f5d2cc
af016fbd6b af016fbd6b
radix 66d2cdb891459f2 https://github.com/mediocregopher/radix
74a799f06760efa
6f8f159a8c
raymond b565731e1464263 https://github.com/aymerick/raymond raymond b565731e1464263 https://github.com/aymerick/raymond
de0bda75f2e45d9 de0bda75f2e45d9
7b54b60110 7b54b60110
redigo 37c69a26f6a2fb5 https://github.com/gomodule/redigo go-redis 7125bf611e5d7d9 https://github.com/go-redis/redis
6c2caa506b29280 bb4487dd6cb80d8
2203a90042 88bad92d23
schema 1f5dc3fa1ac5179 https://github.com/iris-contrib/schema schema 1f5dc3fa1ac5179 https://github.com/iris-contrib/schema
78c014cb1df9954 78c014cb1df9954
0fa5b17f7e 0fa5b17f7e

View File

@ -6,8 +6,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions"
"github.com/kataras/iris/v12/sessions/sessiondb/redis" "github.com/kataras/iris/v12/sessions/sessiondb/redis"
@ -24,24 +22,18 @@ func main() {
Addr: getenv("REDIS_ADDR", "127.0.0.1:6379"), Addr: getenv("REDIS_ADDR", "127.0.0.1:6379"),
Timeout: time.Duration(30) * time.Second, Timeout: time.Duration(30) * time.Second,
MaxActive: 10, MaxActive: 10,
Username: "",
Password: "", Password: "",
Database: "", Database: "",
Prefix: "", Prefix: "",
Delim: "-", Driver: redis.GoRedis(), // defautls.
Driver: redis.Redigo(), // redis.Radix() can be used instead.
}) })
// Optionally configure the underline driver: // Optionally configure the underline driver:
// driver := redis.Redigo() // driver := redis.GoRedis()
// driver.MaxIdle = ... // driver.ClientOptions = redis.Options{...}
// driver.IdleTimeout = ... // driver.ClusterOptions = redis.ClusterOptions{...}
// driver.Wait = ... // redis.New(redis.Config{Driver: driver, ...})
// redis.Config {Driver: driver}
// Close connection when control+C/cmd+C
iris.RegisterOnInterrupt(func() {
db.Close()
})
defer db.Close() // close the database connection if application errored. defer db.Close() // close the database connection if application errored.

View File

@ -57,12 +57,17 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
session := sessions.Get(ctx) session := sessions.Get(ctx)
session.Set("struct", BusinessModel{Name: "John Doe"}) 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) { app.Get("/get-struct", func(ctx iris.Context) {
session := sessions.Get(ctx) 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) { 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"}} business := []BusinessModel{{Name: "Edward"}, {Name: "value 2"}}
session.SetImmutable("businessEdit", business) 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 // 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. // 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) { app.Get("/get-immutable", func(ctx iris.Context) {
valSlice := sessions.Get(ctx).Get("businessEdit") var models []BusinessModel
if valSlice == nil { 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 <a href='/set_immutable'>/set-immutable</a> first") ctx.HTML("please navigate to the <a href='/set_immutable'>/set-immutable</a> first")
return return
} }
firstModel := valSlice.([]BusinessModel)[0] firstModel := models[0]
// businessGet[0].Name is equal to Edward initially // businessGet[0].Name is equal to Edward initially
if firstModel.Name != "Edward" { if firstModel.Name != "Edward" {
panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable") panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable")

17
go.mod
View File

@ -1,18 +1,18 @@
module github.com/kataras/iris/v12 module github.com/kataras/iris/v12
go 1.14 go 1.15
require ( require (
github.com/BurntSushi/toml v0.3.1 github.com/BurntSushi/toml v0.3.1
github.com/CloudyKit/jet/v5 v5.0.3 github.com/CloudyKit/jet/v5 v5.0.3
github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398 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/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible
github.com/dgraph-io/badger/v2 v2.2007.2 github.com/dgraph-io/badger/v2 v2.2007.2
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
github.com/fatih/structs v1.1.0 github.com/fatih/structs v1.1.0
github.com/flosch/pongo2/v4 v4.0.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/google/uuid v1.1.2
github.com/hashicorp/go-version v1.2.1 github.com/hashicorp/go-version v1.2.1
github.com/iris-contrib/httpexpect/v2 v2.0.5 github.com/iris-contrib/httpexpect/v2 v2.0.5
@ -25,20 +25,19 @@ require (
github.com/kataras/pio v0.0.10 github.com/kataras/pio v0.0.10
github.com/kataras/sitemap v0.0.5 github.com/kataras/sitemap v0.0.5
github.com/kataras/tunnel v0.0.2 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/mailru/easyjson v0.7.6
github.com/mediocregopher/radix/v3 v3.5.2
github.com/microcosm-cc/bluemonday v1.0.4 github.com/microcosm-cc/bluemonday v1.0.4
github.com/russross/blackfriday/v2 v2.0.1 github.com/russross/blackfriday/v2 v2.0.1
github.com/schollz/closestmatch v2.1.0+incompatible github.com/schollz/closestmatch v2.1.0+incompatible
github.com/square/go-jose/v3 v3.0.0-20200630053402-0a67ce9b0693 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/vmihailenco/msgpack/v5 v5.0.0-beta.1
github.com/yosssi/ace v0.0.5 github.com/yosssi/ace v0.0.5
go.etcd.io/bbolt v1.3.5 go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
golang.org/x/net v0.0.0-20200927032502-5d4f70055728 golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c
golang.org/x/sys v0.0.0-20200929083018-4d22bbb62b3c golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
golang.org/x/text v0.3.3 golang.org/x/text v0.3.3
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
google.golang.org/protobuf v1.25.0 google.golang.org/protobuf v1.25.0

View File

@ -2,6 +2,7 @@ package sessions
import ( import (
"errors" "errors"
"reflect"
"sync" "sync"
"time" "time"
@ -40,20 +41,24 @@ type Database interface {
OnUpdateExpiration(sid string, newExpires time.Duration) error OnUpdateExpiration(sid string, newExpires time.Duration) error
// Set sets a key value of a specific session. // Set sets a key value of a specific session.
// The "immutable" input argument depends on the store, it may not implement it at all. // 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 retrieves a session value based on the key.
Get(sid string, key string) interface{} 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 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 returns the length of the session's entries (keys).
Len(sid string) int Len(sid string) int
// Delete removes a session key value based on its key. // Delete removes a session key value based on its key.
Delete(sid string, key string) (deleted bool) Delete(sid string, key string) (deleted bool)
// Clear removes all session key values but it keeps the session entry. // 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, // 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. // 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 { 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 } func (s *mem) OnUpdateExpiration(string, time.Duration) error { return nil }
// immutable depends on the store, it may not implement it at all. // 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.mu.RLock()
s.values[sid].Save(key, value, immutable) s.values[sid].Save(key, value, immutable)
s.mu.RUnlock() s.mu.RUnlock()
return nil
} }
func (s *mem) Get(sid string, key string) interface{} { func (s *mem) Get(sid string, key string) interface{} {
@ -92,8 +99,19 @@ func (s *mem) Get(sid string, key string) interface{} {
return v 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) s.values[sid].Visit(cb)
return nil
} }
func (s *mem) Len(sid string) int { func (s *mem) Len(sid string) int {
@ -111,14 +129,20 @@ func (s *mem) Delete(sid string, key string) (deleted bool) {
return return
} }
func (s *mem) Clear(sid string) { func (s *mem) Clear(sid string) error {
s.mu.Lock() s.mu.Lock()
s.values[sid].Reset() s.values[sid].Reset()
s.mu.Unlock() s.mu.Unlock()
return nil
} }
func (s *mem) Release(sid string) { func (s *mem) Release(sid string) error {
s.mu.Lock() s.mu.Lock()
delete(s.values, sid) delete(s.values, sid)
s.mu.Unlock() s.mu.Unlock()
return nil
} }
func (s *mem) Close() error { return nil }

View File

@ -77,6 +77,7 @@ func (p *provider) newSession(man *Sessions, sid string, expires time.Duration)
// Init creates the session and returns it // Init creates the session and returns it
func (p *provider) Init(man *Sessions, sid string, expires time.Duration) *Session { func (p *provider) Init(man *Sessions, sid string, expires time.Duration) *Session {
newSession := p.newSession(man, sid, expires) newSession := p.newSession(man, sid, expires)
newSession.isNew = true
p.mu.Lock() p.mu.Lock()
p.sessions[sid] = newSession p.sessions[sid] = newSession
p.mu.Unlock() 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 // Read returns the store which sid parameter belongs
func (p *provider) Read(man *Sessions, sid string, expires time.Duration) *Session { func (p *provider) Read(man *Sessions, sid string, expires time.Duration) *Session {
p.mu.RLock() 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 sess.runFlashGC() // run the flash messages GC, new request here of existing session
p.mu.RUnlock()
return sess return sess
} }
p.mu.RUnlock()
return p.Init(man, sid, expires) // if not found create new return p.Init(man, sid, expires) // if not found create new
} }

View File

@ -53,7 +53,7 @@ func (s *Session) ID() string {
return s.sid 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. // created by the current application's process.
func (s *Session) IsNew() bool { func (s *Session) IsNew() bool {
return s.isNew return s.isNew
@ -64,6 +64,11 @@ func (s *Session) Get(key string) interface{} {
return s.provider.db.Get(s.sid, key) 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. // when running on the session manager removes any 'old' flash messages.
func (s *Session) runFlashGC() { func (s *Session) runFlashGC() {
s.mu.Lock() s.mu.Lock()
@ -517,11 +522,7 @@ func (s *Session) Len() int {
} }
func (s *Session) set(key string, value interface{}, immutable bool) { func (s *Session) set(key string, value interface{}, immutable bool) {
s.provider.db.Set(s.sid, s.Lifetime, key, value, immutable) s.provider.db.Set(s.sid, key, value, s.Lifetime.DurationUntilExpiration(), immutable)
s.mu.Lock()
s.isNew = false
s.mu.Unlock()
} }
// Set fills the session with an entry "value", based on its "key". // 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. // returns true if actually something was removed.
func (s *Session) Delete(key string) bool { func (s *Session) Delete(key string) bool {
removed := s.provider.db.Delete(s.sid, key) removed := s.provider.db.Delete(s.sid, key)
if removed {
s.mu.Lock()
s.isNew = false
s.mu.Unlock()
}
return removed return removed
} }
@ -587,10 +582,7 @@ func (s *Session) DeleteFlash(key string) {
// Clear removes all entries. // Clear removes all entries.
func (s *Session) Clear() { func (s *Session) Clear() {
s.mu.Lock()
s.provider.db.Clear(s.sid) s.provider.db.Clear(s.sid)
s.isNew = false
s.mu.Unlock()
} }
// ClearFlashes removes all flash messages. // ClearFlashes removes all flash messages.

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"errors" "errors"
"os" "os"
"runtime"
"sync/atomic" "sync/atomic"
"time" "time"
@ -71,7 +70,7 @@ func New(directoryPath string) (*Database, error) {
func NewFromDB(service *badger.DB) *Database { func NewFromDB(service *badger.DB) *Database {
db := &Database{Service: service} db := &Database{Service: service}
runtime.SetFinalizer(db, closeDB) // runtime.SetFinalizer(db, closeDB)
return db return db
} }
@ -127,25 +126,35 @@ func makeKey(sid, key string) []byte {
// Set sets a key value of a specific session. // Set sets a key value of a specific session.
// Ignore the "immutable". // 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) valueBytes, err := sessions.DefaultTranscoder.Marshal(value)
if err != nil { if err != nil {
db.logger.Error(err) db.logger.Error(err)
return return err
} }
err = db.Service.Update(func(txn *badger.Txn) error { err = db.Service.Update(func(txn *badger.Txn) error {
dur := lifetime.DurationUntilExpiration() return txn.SetEntry(badger.NewEntry(makeKey(sid, key), valueBytes).WithTTL(ttl))
return txn.SetEntry(badger.NewEntry(makeKey(sid, key), valueBytes).WithTTL(dur))
}) })
if err != nil { if err != nil {
db.logger.Error(err) db.logger.Error(err)
} }
return err
} }
// Get retrieves a session value based on the key. // Get retrieves a session value based on the key.
func (db *Database) Get(sid string, key string) (value interface{}) { 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 { err := db.Service.View(func(txn *badger.Txn) error {
item, err := txn.Get(makeKey(sid, key)) item, err := txn.Get(makeKey(sid, key))
if err != nil { 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 item.Value(func(valueBytes []byte) error {
return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) return sessions.DefaultTranscoder.Unmarshal(valueBytes, outPtr)
}) })
}) })
if err != nil && err != badger.ErrKeyNotFound { if err != nil && err != badger.ErrKeyNotFound {
db.logger.Error(err) db.logger.Error(err)
return nil
} }
return return err
} }
// validSessionItem reports whether the current iterator's item key // 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. // 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) prefix := makePrefix(sid)
txn := db.Service.NewTransaction(false) txn := db.Service.NewTransaction(false)
@ -199,11 +207,13 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) {
}) })
if err != nil { if err != nil {
db.logger.Errorf("[sessionsdb.badger.Visit] %v", err) db.logger.Errorf("[sessionsdb.badger.Visit] %v", err)
continue return err
} }
cb(string(bytes.TrimPrefix(key, prefix)), value) cb(string(bytes.TrimPrefix(key, prefix)), value)
} }
return nil
} }
var iterOptionsNoValues = badger.IteratorOptions{ 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. // 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) prefix := makePrefix(sid)
txn := db.Service.NewTransaction(true) txn := db.Service.NewTransaction(true)
@ -260,24 +270,33 @@ func (db *Database) Clear(sid string) {
key := iter.Item().Key() key := iter.Item().Key()
if err := txn.Delete(key); err != nil { if err := txn.Delete(key); err != nil {
db.logger.Warnf("Database.Clear: %s: %v", key, err) 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, // 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. // 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. // clear all $sid-$key.
db.Clear(sid) err := db.Clear(sid)
if err != nil {
return err
}
// and remove the $sid. // and remove the $sid.
txn := db.Service.NewTransaction(true) 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) 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) db.logger.Debugf("Database.Release.Commit: %s: %v", sid, err)
return err
} }
return nil
} }
// Close shutdowns the badger connection. // Close shutdowns the badger connection.

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"time" "time"
"github.com/kataras/iris/v12/sessions" "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} db := &Database{table: bucket, Service: service}
runtime.SetFinalizer(db, closeDB) // runtime.SetFinalizer(db, closeDB)
return db, db.cleanup() return db, db.cleanup()
} }
@ -254,11 +253,11 @@ func makeKey(key string) []byte {
// Set sets a key value of a specific session. // Set sets a key value of a specific session.
// Ignore the "immutable". // 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) valueBytes, err := sessions.DefaultTranscoder.Marshal(value)
if err != nil { if err != nil {
db.logger.Debug(err) db.logger.Debug(err)
return return err
} }
err = db.Service.Update(func(tx *bolt.Tx) error { 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 { if err != nil {
db.logger.Debug(err) db.logger.Debug(err)
} }
return err
} }
// Get retrieves a session value based on the key. // Get retrieves a session value based on the key.
func (db *Database) Get(sid string, key string) (value interface{}) { 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 { err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid) b := db.getBucketForSession(tx, sid)
if b == nil { if b == nil {
@ -292,17 +302,17 @@ func (db *Database) Get(sid string, key string) (value interface{}) {
return nil return nil
} }
return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) return sessions.DefaultTranscoder.Unmarshal(valueBytes, outPtr)
}) })
if err != nil { if err != nil {
db.logger.Debugf("session '%s' key '%s' cannot be retrieved: %v", sid, key, err) 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. // 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 { err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid) b := db.getBucketForSession(tx, sid)
if b == nil { if b == nil {
@ -324,17 +334,19 @@ func (db *Database) Visit(sid string, cb func(key string, value interface{})) {
if err != nil { if err != nil {
db.logger.Debugf("Database.Visit: %s: %v", sid, err) db.logger.Debugf("Database.Visit: %s: %v", sid, err)
} }
return err
} }
// Len returns the length of the session's entries (keys). // 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 { err := db.Service.View(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid) b := db.getBucketForSession(tx, sid)
if b == nil { if b == nil {
return nil return nil
} }
n = b.Stats().KeyN n = int64(b.Stats().KeyN)
return nil 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. // 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 { err := db.Service.Update(func(tx *bolt.Tx) error {
b := db.getBucketForSession(tx, sid) b := db.getBucketForSession(tx, sid)
if b == nil { if b == nil {
@ -375,11 +387,13 @@ func (db *Database) Clear(sid string) {
if err != nil { if err != nil {
db.logger.Debugf("Database.Clear: %s: %v", sid, err) db.logger.Debugf("Database.Clear: %s: %v", sid, err)
} }
return err
} }
// Release destroys the session, it clears and removes the session entry, // 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. // 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 { err := db.Service.Update(func(tx *bolt.Tx) error {
// delete the session bucket. // delete the session bucket.
b := db.getBucket(tx) b := db.getBucket(tx)
@ -393,6 +407,8 @@ func (db *Database) Release(sid string) {
if err != nil { if err != nil {
db.logger.Debugf("Database.Release: %s: %v", sid, err) db.logger.Debugf("Database.Release: %s: %v", sid, err)
} }
return err
} }
// Close shutdowns the BoltDB connection. // Close shutdowns the BoltDB connection.

View File

@ -3,7 +3,7 @@ package redis
import ( import (
"crypto/tls" "crypto/tls"
"errors" "errors"
"strings" "fmt"
"time" "time"
"github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions"
@ -18,8 +18,6 @@ const (
DefaultRedisAddr = "127.0.0.1:6379" DefaultRedisAddr = "127.0.0.1:6379"
// DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second // DefaultRedisTimeout the redis idle timeout option, time.Duration(30) * time.Second
DefaultRedisTimeout = time.Duration(30) * time.Second DefaultRedisTimeout = time.Duration(30) * time.Second
// DefaultDelim ths redis delim option, "-".
DefaultDelim = "-"
) )
// Config the redis configuration used inside sessions // Config the redis configuration used inside sessions
@ -31,31 +29,36 @@ type Config struct {
// Defaults to "127.0.0.1:6379". // Defaults to "127.0.0.1:6379".
Addr string Addr string
// Clusters a list of network addresses for clusters. // Clusters a list of network addresses for clusters.
// If not empty "Addr" is ignored. // If not empty "Addr" is ignored and Redis clusters feature is used instead.
// Currently only Radix() Driver supports it.
Clusters []string 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 Password string
// If Database is empty "" then no 'SELECT'. Defaults to "". // If Database is empty "" then no 'SELECT'. Defaults to "".
Database string 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 MaxActive int
// Timeout for connect, write and read, defaults to 30 seconds, 0 means no timeout. // Timeout for connect, write and read, defaults to 30 seconds, 0 means no timeout.
Timeout time.Duration Timeout time.Duration
// Prefix "myprefix-for-this-website". Defaults to "". // Prefix "myprefix-for-this-website". Defaults to "".
Prefix string 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 // TLSConfig will cause Dial to perform a TLS handshake using the provided
// config. If is nil then no TLS is used. // config. If is nil then no TLS is used.
// See https://golang.org/pkg/crypto/tls/#Config // See https://golang.org/pkg/crypto/tls/#Config
TLSConfig *tls.Config TLSConfig *tls.Config
// Driver supports `Redigo()` or `Radix()` go clients for redis. // A Driver should support be a go client for redis communication.
// Configure each driver by the return value of their constructors. // It can be set to a custom one or a mock one (for testing).
// //
// Defaults to `Redigo()`. // Defaults to `GoRedis()`.
Driver Driver Driver Driver
} }
@ -64,14 +67,14 @@ func DefaultConfig() Config {
return Config{ return Config{
Network: DefaultRedisNetwork, Network: DefaultRedisNetwork,
Addr: DefaultRedisAddr, Addr: DefaultRedisAddr,
Username: "",
Password: "", Password: "",
Database: "", Database: "",
MaxActive: 10, MaxActive: 10,
Timeout: DefaultRedisTimeout, Timeout: DefaultRedisTimeout,
Prefix: "", Prefix: "",
Delim: DefaultDelim,
TLSConfig: nil, TLSConfig: nil,
Driver: Redigo(), Driver: GoRedis(),
} }
} }
@ -83,7 +86,7 @@ type Database struct {
var _ sessions.Database = (*Database)(nil) var _ sessions.Database = (*Database)(nil)
// New returns a new redis database. // New returns a new redis sessions database.
func New(cfg ...Config) *Database { func New(cfg ...Config) *Database {
c := DefaultConfig() c := DefaultConfig()
if len(cfg) > 0 { if len(cfg) > 0 {
@ -101,16 +104,8 @@ func New(cfg ...Config) *Database {
c.Addr = DefaultRedisAddr c.Addr = DefaultRedisAddr
} }
if c.MaxActive == 0 {
c.MaxActive = 10
}
if c.Delim == "" {
c.Delim = DefaultDelim
}
if c.Driver == nil { if c.Driver == nil {
c.Driver = Redigo() c.Driver = GoRedis()
} }
} }
@ -127,93 +122,108 @@ func New(cfg ...Config) *Database {
return db 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. // SetLogger sets the logger once before server ran.
// By default the Iris one is injected. // By default the Iris one is injected.
func (db *Database) SetLogger(logger *golog.Logger) { func (db *Database) SetLogger(logger *golog.Logger) {
db.logger = 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, // 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. // 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 { func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime {
key := db.makeKey(sid, "") sidKey := db.makeSID(sid)
seconds, hasExpiration, found := db.c.Driver.TTL(key) if !db.c.Driver.Exists(sidKey) {
if !found { if err := db.Set(sidKey, SessionIDKey, sid, 0, false); err != nil {
// 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) 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. return sessions.LifeTime{} // session manager will handle the rest.
} }
if !hasExpiration { untilExpire := db.c.Driver.TTL(sidKey)
return sessions.LifeTime{} return sessions.LifeTime{Time: time.Now().Add(untilExpire)}
}
return sessions.LifeTime{Time: time.Now().Add(time.Duration(seconds) * time.Second)}
} }
// OnUpdateExpiration will re-set the database's session's entry ttl. // OnUpdateExpiration will re-set the database's session's entry ttl.
// https://redis.io/commands/expire#refreshing-expires // https://redis.io/commands/expire#refreshing-expires
func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error {
return db.c.Driver.UpdateTTLMany(db.makeKey(sid, ""), int64(newExpires.Seconds())) return db.c.Driver.UpdateTTL(db.makeSID(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. // Set sets a key value of a specific session.
// Ignore the "immutable". // 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) valueBytes, err := sessions.DefaultTranscoder.Marshal(value)
if err != nil { if err != nil {
db.logger.Error(err) 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 return err
} }
if err = sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr); err != nil { if err = db.c.Driver.Set(db.makeSID(sid), key, valueBytes); err != nil {
db.logger.Debugf("unable to unmarshal value of key: '%s': %v", key, err) db.logger.Debug(err)
return err return err
} }
return nil return nil
} }
func (db *Database) keys(sid string) []string { // Get retrieves a session value based on the key.
keys, err := db.c.Driver.GetKeys(db.makeKey(sid, "")) 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 { 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 return nil
} }
@ -221,24 +231,33 @@ func (db *Database) keys(sid string) []string {
} }
// Visit loops through all session keys and values. // 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 {
keys := db.keys(sid) kv, err := db.c.Driver.GetAll(db.makeSID(sid))
for _, key := range keys { if err != nil {
var value interface{} // new value each time, we don't know what user will do in "cb". return err
db.get(key, &value)
key = strings.TrimPrefix(key, db.c.Prefix+sid+db.c.Delim)
cb(key, value)
} }
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). // Len returns the length of the session's entries (keys).
func (db *Database) Len(sid string) (n int) { func (db *Database) Len(sid string) int {
return len(db.keys(sid)) return db.c.Driver.Len(sid)
} }
// Delete removes a session key value based on its key. // Delete removes a session key value based on its key.
func (db *Database) Delete(sid string, key string) (deleted bool) { 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 { if err != nil {
db.logger.Error(err) 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. // 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 {
keys := db.keys(sid) keys := db.keys(db.makeSID(sid))
for _, key := range keys { 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) 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, // 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. // 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. err := db.c.Driver.Delete(db.makeSID(sid), "")
db.Clear(sid)
// and remove the $sid.
err := db.c.Driver.Delete(db.c.Prefix + sid)
if err != nil { if err != nil {
db.logger.Debugf("Database.Release.Driver.Delete: %s: %v", sid, err) db.logger.Debugf("Database.Release.Driver.Delete: %s: %v", sid, err)
} }
return err
} }
// Close terminates the redis connection. // Close terminates the redis connection.

View File

@ -1,34 +1,30 @@
package redis package redis
import "time"
// Driver is the interface which each supported redis client // Driver is the interface which each supported redis client
// should support in order to be used in the redis session database. // should support in order to be used in the redis session database.
type Driver interface { type Driver interface {
Connect(c Config) error Connect(c Config) error
PingPong() (bool, error) PingPong() (bool, error)
CloseConnection() error CloseConnection() error
Set(key string, value interface{}, secondsLifetime int64) error Set(sid, key string, value interface{}) error
Get(key string) (interface{}, error) Get(sid, key string) (interface{}, error)
TTL(key string) (seconds int64, hasExpiration bool, found bool) Exists(sid string) bool
UpdateTTL(key string, newSecondsLifeTime int64) error TTL(sid string) time.Duration
UpdateTTLMany(prefix string, newSecondsLifeTime int64) error UpdateTTL(sid string, newLifetime time.Duration) error
GetAll() (interface{}, error) GetAll(sid string) (map[string]string, error)
GetKeys(prefix string) ([]string, error) GetKeys(sid string) ([]string, error)
Delete(key string) error Len(sid string) int
Delete(sid, key string) error
} }
var ( var (
_ Driver = (*RedigoDriver)(nil) _ Driver = (*GoRedisDriver)(nil)
_ Driver = (*RadixDriver)(nil)
) )
// Redigo returns the driver for the redigo go redis client. // GoRedis returns the default Driver for the redis sessions database
// Which is the default one. // It's the go-redis client. Learn more at: https://github.com/go-redis/redis.
// You can customize further any specific driver's properties. func GoRedis() *GoRedisDriver {
func Redigo() *RedigoDriver { return &GoRedisDriver{}
return &RedigoDriver{}
}
// Radix returns the driver for the radix go redis client.
func Radix() *RadixDriver {
return &RadixDriver{}
} }

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/core/host"
) )
func init() { func init() {
@ -51,6 +52,9 @@ func New(cfg Config) *Sessions {
// a session db doesn't have write access // a session db doesn't have write access
func (s *Sessions) UseDatabase(db Database) { func (s *Sessions) UseDatabase(db Database) {
db.SetLogger(s.config.Logger) // inject the logger. db.SetLogger(s.config.Logger) // inject the logger.
host.RegisterOnInterrupt(func() {
db.Close()
})
s.provider.RegisterDatabase(db) 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) // fmt.Printf("%s=%s\n", key, value)
// }) // })
// } // }
sess.isNew = s.provider.db.Len(sid) == 0
s.updateCookie(ctx, sid, s.config.Expires, cookieOptions...) s.updateCookie(ctx, sid, s.config.Expires, cookieOptions...)
return sess return sess