From 16d1a8def0b22ee8ae14cca95fbf8c4b1d7f19fc Mon Sep 17 00:00:00 2001 From: James Elliott Date: Fri, 26 Feb 2021 11:01:04 +1100 Subject: [PATCH] feat(provider): redisfailover (sentinel) implementation (#2) * feat(redis): implement username option (#36) Allows defining of the username to accompany the password. * Upgrade dependencies * Upgrade fasthttp to v1.20.0 * Upgrade fasthttp to v1.21.0 and other dependencies * Add support to Go v1.16.X * feat(provider): add redis failover provider (sentinel) Co-authored-by: Sergio Andres Virviescas Santana --- .travis.yml | 0 _examples/main.go | 2 +- go.mod | 6 +- go.sum | 12 +-- providers/memory/provider.go | 1 + providers/redisfailover/README.md | 8 ++ providers/redisfailover/errors.go | 12 +++ providers/redisfailover/provider.go | 137 ++++++++++++++++++++++++++++ providers/redisfailover/types.go | 105 +++++++++++++++++++++ 9 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 .travis.yml create mode 100644 providers/redisfailover/README.md create mode 100644 providers/redisfailover/errors.go create mode 100644 providers/redisfailover/provider.go create mode 100644 providers/redisfailover/types.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..e69de29 diff --git a/_examples/main.go b/_examples/main.go index ae0d43d..1ed61f3 100644 --- a/_examples/main.go +++ b/_examples/main.go @@ -5,7 +5,6 @@ import ( "log" "time" - "github.com/fasthttp/router" "github.com/authelia/session/v2" "github.com/authelia/session/v2/providers/memcache" "github.com/authelia/session/v2/providers/memory" @@ -13,6 +12,7 @@ import ( "github.com/authelia/session/v2/providers/postgre" "github.com/authelia/session/v2/providers/redis" "github.com/authelia/session/v2/providers/sqlite3" + "github.com/fasthttp/router" "github.com/valyala/fasthttp" ) diff --git a/go.mod b/go.mod index ae5e66b..4be8a6d 100644 --- a/go.mod +++ b/go.mod @@ -8,8 +8,8 @@ require ( github.com/go-sql-driver/mysql v1.5.0 github.com/lib/pq v1.9.0 github.com/mattn/go-sqlite3 v1.14.6 - github.com/savsgio/dictpool v0.0.0-20210105101557-9da1bc2fbfce - github.com/savsgio/gotils v0.0.0-20210105085219-0567298fdcac + github.com/savsgio/dictpool v0.0.0-20210217113430-85d3b37fb239 + github.com/savsgio/gotils v0.0.0-20210217112953-d4a072536008 github.com/valyala/bytebufferpool v1.0.0 - github.com/valyala/fasthttp v1.19.0 + github.com/valyala/fasthttp v1.21.0 ) diff --git a/go.sum b/go.sum index 948d406..e001c66 100644 --- a/go.sum +++ b/go.sum @@ -49,10 +49,10 @@ github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/savsgio/dictpool v0.0.0-20210105101557-9da1bc2fbfce h1:PRDREQ3VGiocUySEdKYQpwdyxDx+e4uKdjVaQvwIR5I= -github.com/savsgio/dictpool v0.0.0-20210105101557-9da1bc2fbfce/go.mod h1:TNr2IIMnYd9/KYEpTVHVrnfmjizlKPTSgkWUbjyof+A= -github.com/savsgio/gotils v0.0.0-20210105085219-0567298fdcac h1:5pr1F6tcjeIKPKlrQZW6hzB0WxZge7SkkYVGG/e5pLY= -github.com/savsgio/gotils v0.0.0-20210105085219-0567298fdcac/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8= +github.com/savsgio/dictpool v0.0.0-20210217113430-85d3b37fb239 h1:aTxmMsYGLUZfj0EsWaJ1s0HnctxCgjRw3A+TFoO1Tsc= +github.com/savsgio/dictpool v0.0.0-20210217113430-85d3b37fb239/go.mod h1:CfPSewBwpXF/05Izyk9s379O1ysmtUajFVr1nOD83Fs= +github.com/savsgio/gotils v0.0.0-20210217112953-d4a072536008 h1:GfiZ0x43l1tOeyam9RAlJaUkxPwGRz3bIbmtyfTZIWY= +github.com/savsgio/gotils v0.0.0-20210217112953-d4a072536008/go.mod h1:TWNAOTaVzGOXq8RbEvHnhzA/A2sLZzgn0m6URjnukY8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -61,8 +61,8 @@ github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk1 github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.19.0 h1:PfTS4PeH3xDr3WomrDS2ID8lU2GskK1xS3YG6gIpibU= -github.com/valyala/fasthttp v1.19.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= +github.com/valyala/fasthttp v1.21.0 h1:fJjaQ7cXdaSF9vDBujlHLDGj7AgoMTMIXvICeePzYbU= +github.com/valyala/fasthttp v1.21.0/go.mod h1:jjraHZVbKOXftJfsOYoAjaeygpj5hr8ermTRJNroD7A= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opentelemetry.io/otel v0.13.0 h1:2isEnyzjjJZq6r2EKMsFj4TxiQiexsM04AVhwbR/oBA= diff --git a/providers/memory/provider.go b/providers/memory/provider.go index 4419a59..93b7916 100644 --- a/providers/memory/provider.go +++ b/providers/memory/provider.go @@ -3,6 +3,7 @@ package memory import ( "sync" "time" + "github.com/authelia/session/v2" "github.com/savsgio/gotils/strconv" ) diff --git a/providers/redisfailover/README.md b/providers/redisfailover/README.md new file mode 100644 index 0000000..a0d52f3 --- /dev/null +++ b/providers/redisfailover/README.md @@ -0,0 +1,8 @@ +# Redis Fail Over + +Redis Fail Over (Sentinel) provider implementation. + +Better encoder: + +- Encode: `session.MSGPEncode` +- Decode: `session.MSGPDecode` diff --git a/providers/redisfailover/errors.go b/providers/redisfailover/errors.go new file mode 100644 index 0000000..461ff28 --- /dev/null +++ b/providers/redisfailover/errors.go @@ -0,0 +1,12 @@ +package redisfailover + +import ( + "errors" + "fmt" +) + +var errConfigMasterNameEmpty = errors.New("Config MasterName must not be empty") + +func errRedisConnection(err error) error { + return fmt.Errorf("Redis connection error: %v", err) +} diff --git a/providers/redisfailover/provider.go b/providers/redisfailover/provider.go new file mode 100644 index 0000000..18cd6c9 --- /dev/null +++ b/providers/redisfailover/provider.go @@ -0,0 +1,137 @@ +package redisfailover + +import ( + "context" + "time" + + "github.com/go-redis/redis/v8" + "github.com/valyala/bytebufferpool" +) + +var all = []byte("*") + +// New returns a new configured redis provider +func New(cfg Config) (*Provider, error) { + if cfg.MasterName == "" { + return nil, errConfigMasterNameEmpty + } + + db := redis.NewFailoverClient(&redis.FailoverOptions{ + MasterName: cfg.MasterName, + SentinelAddrs: cfg.SentinelAddrs, + RouteByLatency: cfg.RouteByLatency, + RouteRandomly: cfg.RouteRandomly, + SlaveOnly: cfg.SlaveOnly, + Username: cfg.Username, + Password: cfg.Password, + DB: cfg.DB, + MaxRetries: cfg.MaxRetries, + MinRetryBackoff: cfg.MinRetryBackoff, + MaxRetryBackoff: cfg.MaxRetryBackoff, + DialTimeout: cfg.DialTimeout, + ReadTimeout: cfg.ReadTimeout, + WriteTimeout: cfg.WriteTimeout, + PoolSize: cfg.PoolSize, + MinIdleConns: cfg.MinIdleConns, + MaxConnAge: cfg.MaxConnAge, + PoolTimeout: cfg.PoolTimeout, + IdleTimeout: cfg.IdleTimeout, + IdleCheckFrequency: cfg.IdleCheckFrequency, + TLSConfig: cfg.TLSConfig, + }) + + if err := db.Ping(context.Background()).Err(); err != nil { + return nil, errRedisConnection(err) + } + + p := &Provider{ + config: cfg, + db: db, + } + + return p, nil +} + +func (p *Provider) getRedisSessionKey(sessionID []byte) string { + key := bytebufferpool.Get() + key.SetString(p.config.KeyPrefix) + key.WriteString(":") + key.Write(sessionID) + + keyStr := key.String() + + bytebufferpool.Put(key) + + return keyStr +} + +// Get returns the data of the given session id +func (p *Provider) Get(id []byte) ([]byte, error) { + key := p.getRedisSessionKey(id) + + reply, err := p.db.Get(context.Background(), key).Bytes() + if err != nil && err != redis.Nil { + return nil, err + } + + return reply, nil + +} + +// Save saves the session data and expiration from the given session id +func (p *Provider) Save(id, data []byte, expiration time.Duration) error { + key := p.getRedisSessionKey(id) + + return p.db.Set(context.Background(), key, data, expiration).Err() +} + +// Regenerate updates the session id and expiration with the new session id +// of the the given current session id +func (p *Provider) Regenerate(id, newID []byte, expiration time.Duration) error { + key := p.getRedisSessionKey(id) + newKey := p.getRedisSessionKey(newID) + + exists, err := p.db.Exists(context.Background(), key).Result() + if err != nil { + return err + } + + if exists > 0 { // Exist + if err = p.db.Rename(context.Background(), key, newKey).Err(); err != nil { + return err + } + + if err = p.db.Expire(context.Background(), newKey, expiration).Err(); err != nil { + return err + } + } + + return nil +} + +// Destroy destroys the session from the given id +func (p *Provider) Destroy(id []byte) error { + key := p.getRedisSessionKey(id) + + return p.db.Del(context.Background(), key).Err() +} + +// Count returns the total of stored sessions +func (p *Provider) Count() int { + reply, err := p.db.Keys(context.Background(), p.getRedisSessionKey(all)).Result() + if err != nil { + return 0 + } + + return len(reply) +} + +// NeedGC indicates if the GC needs to be run +func (p *Provider) NeedGC() bool { + return false +} + +// GC destroys the expired sessions +func (p *Provider) GC() error { + return nil +} diff --git a/providers/redisfailover/types.go b/providers/redisfailover/types.go new file mode 100644 index 0000000..1c65324 --- /dev/null +++ b/providers/redisfailover/types.go @@ -0,0 +1,105 @@ +package redisfailover + +import ( + "crypto/tls" + "time" + + "github.com/go-redis/redis/v8" +) + +// Config provider settings +type Config struct { + // Key prefix + KeyPrefix string + + // The sentinel master name. + MasterName string + + // The sentinel nodes seed list (host:port). + SentinelAddrs []string + + // The password for the sentinel connection if required (different to username/password). + SentinelPassword string + + // Routes read-only commands to the closest node. + RouteByLatency bool + + // Routes read-only commands in random order. + RouteRandomly bool + + // Route read-only commands to slave nodes. + SlaveOnly bool + + // Optional username. + Username string + + // Optional password. Must match the password specified in the + // requirepass server configuration option. + Password string + + // Database to be selected after connecting to the server. + DB int + + // Maximum number of retries before giving up. + // Default is to not retry failed commands. + MaxRetries int + + // Minimum backoff between each retry. + // Default is 8 milliseconds; -1 disables backoff. + MinRetryBackoff time.Duration + + // Maximum backoff between each retry. + // Default is 512 milliseconds; -1 disables backoff. + MaxRetryBackoff time.Duration + + // Dial timeout for establishing new connections. + // Default is 5 seconds. + DialTimeout time.Duration + + // Timeout for socket reads. If reached, commands will fail + // with a timeout instead of blocking. Use value -1 for no timeout and 0 for default. + // Default is 3 seconds. + ReadTimeout time.Duration + + // Timeout for socket writes. If reached, commands will fail + // with a timeout instead of blocking. + // Default is ReadTimeout. + WriteTimeout time.Duration + + // Maximum number of socket connections. + // Default is 10 connections per every CPU as reported by runtime.NumCPU. + PoolSize int + + // Minimum number of idle connections which is useful when establishing + // new connection is slow. + MinIdleConns int + + // Connection age at which client retires (closes) the connection. + // Default is to not close aged connections. + MaxConnAge time.Duration + + // Amount of time client waits for connection if all connections + // are busy before returning an error. + // Default is ReadTimeout + 1 second. + PoolTimeout time.Duration + + // Amount of time after which client closes idle connections. + // Should be less than server's timeout. + // Default is 5 minutes. -1 disables idle timeout check. + IdleTimeout time.Duration + + // Frequency of idle checks made by idle connections reaper. + // Default is 1 minute. -1 disables idle connections reaper, + // but idle connections are still discarded by the client + // if IdleTimeout is set. + IdleCheckFrequency time.Duration + + // TLS Config to use. When set TLS will be negotiated. + TLSConfig *tls.Config +} + +// Provider backend manager +type Provider struct { + config Config + db *redis.Client +}