|
@@ -0,0 +1,237 @@
|
|
|
+package sessions
|
|
|
+
|
|
|
+import (
|
|
|
+ "encoding/json"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "net/http"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/go-redis/redis"
|
|
|
+ "github.com/gorilla/securecookie"
|
|
|
+ gsessions "github.com/gorilla/sessions"
|
|
|
+ "github.com/teris-io/shortid"
|
|
|
+)
|
|
|
+
|
|
|
+// RediStore stores sessions in a redis backend.
|
|
|
+type RediStore struct {
|
|
|
+ RedisClient *redis.Client
|
|
|
+ Codecs []securecookie.Codec
|
|
|
+ SessionOptions *gsessions.Options // default configuration
|
|
|
+ DefaultMaxAge int // default Redis TTL for a MaxAge == 0 session
|
|
|
+ maxLength int
|
|
|
+ keyPrefix string
|
|
|
+ serializer SessionSerializer
|
|
|
+}
|
|
|
+
|
|
|
+func NewRediStore(client *redis.Client, keyPrefix string, keyPairs ...[]byte) *RediStore {
|
|
|
+ if keyPrefix == "" {
|
|
|
+ keyPrefix = "session_"
|
|
|
+ }
|
|
|
+ rs := &RediStore{
|
|
|
+ RedisClient: client,
|
|
|
+ Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
|
|
+ SessionOptions: &gsessions.Options{
|
|
|
+ Path: defaultPath,
|
|
|
+ MaxAge: defaultMaxAge,
|
|
|
+ },
|
|
|
+ DefaultMaxAge: 60 * 20, // 20 minutes seems like a reasonable default
|
|
|
+ maxLength: 4096,
|
|
|
+ keyPrefix: keyPrefix,
|
|
|
+ serializer: JSONSerializer{},
|
|
|
+ }
|
|
|
+ return rs
|
|
|
+}
|
|
|
+
|
|
|
+// SessionSerializer provides an interface hook for alternative serializers
|
|
|
+type SessionSerializer interface {
|
|
|
+ Deserialize(d []byte, ss *gsessions.Session) error
|
|
|
+ Serialize(ss *gsessions.Session) ([]byte, error)
|
|
|
+}
|
|
|
+
|
|
|
+// JSONSerializer encode the session map to JSON.
|
|
|
+type JSONSerializer struct{}
|
|
|
+
|
|
|
+// Serialize to JSON. Will err if there are unmarshalable key values
|
|
|
+func (s JSONSerializer) Serialize(ss *gsessions.Session) ([]byte, error) {
|
|
|
+ m := make(map[string]interface{}, len(ss.Values))
|
|
|
+ for k, v := range ss.Values {
|
|
|
+ ks, ok := k.(string)
|
|
|
+ if !ok {
|
|
|
+ err := fmt.Errorf("Non-string key value, cannot serialize session to JSON: %v", k)
|
|
|
+ fmt.Printf("redistore.JSONSerializer.serialize() Error: %v", err)
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ m[ks] = v
|
|
|
+ }
|
|
|
+ return json.Marshal(m)
|
|
|
+}
|
|
|
+
|
|
|
+// Deserialize back to map[string]interface{}
|
|
|
+func (s JSONSerializer) Deserialize(d []byte, ss *gsessions.Session) error {
|
|
|
+ m := make(map[string]interface{})
|
|
|
+ err := json.Unmarshal(d, &m)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("redistore.JSONSerializer.deserialize() Error: %v", err)
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ for k, v := range m {
|
|
|
+ ss.Values[k] = v
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// SetMaxLength sets RediStore.maxLength if the `l` argument is greater or equal 0
|
|
|
+// maxLength restricts the maximum length of new sessions to l.
|
|
|
+// If l is 0 there is no limit to the size of a session, use with caution.
|
|
|
+// The default for a new RediStore is 4096. Redis allows for max.
|
|
|
+// value sizes of up to 512MB (http://redis.io/topics/data-types)
|
|
|
+// Default: 4096,
|
|
|
+func (s *RediStore) SetMaxLength(l int) {
|
|
|
+ if l >= 0 {
|
|
|
+ s.maxLength = l
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (s *RediStore) Options(options Options) {
|
|
|
+ s.SessionOptions = &gsessions.Options{
|
|
|
+ Path: options.Path,
|
|
|
+ Domain: options.Domain,
|
|
|
+ MaxAge: options.MaxAge,
|
|
|
+ Secure: options.Secure,
|
|
|
+ HttpOnly: options.HttpOnly,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// SetSerializer sets the serializer
|
|
|
+func (s *RediStore) SetSerializer(ss SessionSerializer) {
|
|
|
+ s.serializer = ss
|
|
|
+}
|
|
|
+
|
|
|
+// SetMaxAge restricts the maximum age, in seconds, of the session record
|
|
|
+// both in database and a browser. This is to change session storage configuration.
|
|
|
+// If you want just to remove session use your session `s` object and change it's
|
|
|
+// `Options.MaxAge` to -1, as specified in
|
|
|
+// http://godoc.org/github.com/gorilla/sessions#Options
|
|
|
+//
|
|
|
+// Default is the one provided by this package value - `sessionExpire`.
|
|
|
+// Set it to 0 for no restriction.
|
|
|
+// Because we use `MaxAge` also in SecureCookie crypting algorithm you should
|
|
|
+// use this function to change `MaxAge` value.
|
|
|
+func (s *RediStore) SetMaxAge(v int) {
|
|
|
+ var c *securecookie.SecureCookie
|
|
|
+ var ok bool
|
|
|
+ s.SessionOptions.MaxAge = v
|
|
|
+ for i := range s.Codecs {
|
|
|
+ if c, ok = s.Codecs[i].(*securecookie.SecureCookie); ok {
|
|
|
+ c.MaxAge(v)
|
|
|
+ } else {
|
|
|
+ fmt.Printf("Can't change MaxAge on codec %v\n", s.Codecs[i])
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Get returns a session for the given name after adding it to the registry.
|
|
|
+//
|
|
|
+// See gorilla/sessions FilesystemStore.Get().
|
|
|
+func (s *RediStore) Get(r *http.Request, name string) (*gsessions.Session, error) {
|
|
|
+ return gsessions.GetRegistry(r).Get(s, name)
|
|
|
+}
|
|
|
+
|
|
|
+// New returns a session for the given name without adding it to the registry.
|
|
|
+//
|
|
|
+// See gorilla/sessions FilesystemStore.New().
|
|
|
+func (s *RediStore) New(r *http.Request, name string) (*gsessions.Session, error) {
|
|
|
+ var (
|
|
|
+ err error
|
|
|
+ ok bool
|
|
|
+ )
|
|
|
+ session := gsessions.NewSession(s, name)
|
|
|
+ // make a copy
|
|
|
+ options := *s.SessionOptions
|
|
|
+ session.Options = &options
|
|
|
+ session.IsNew = true
|
|
|
+ if c, errCookie := r.Cookie(name); errCookie == nil {
|
|
|
+ session.ID = c.Value
|
|
|
+ ok, err = s.load(session)
|
|
|
+ session.IsNew = !(err == nil && ok)
|
|
|
+ } else {
|
|
|
+ session.ID = shortid.MustGenerate()
|
|
|
+ }
|
|
|
+ return session, err
|
|
|
+}
|
|
|
+
|
|
|
+func (s *RediStore) RenewID(r *http.Request, w http.ResponseWriter, session *gsessions.Session) error {
|
|
|
+ _id := session.ID
|
|
|
+ data, err := s.RedisClient.Get(s.keyPrefix + _id).Result()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ session.ID = shortid.MustGenerate()
|
|
|
+ age := session.Options.MaxAge
|
|
|
+ if age == 0 {
|
|
|
+ age = s.DefaultMaxAge
|
|
|
+ }
|
|
|
+ _, err = s.RedisClient.Set(s.keyPrefix+session.ID, data, time.Duration(age)*time.Second).Result()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ http.SetCookie(w, gsessions.NewCookie(session.Name(), session.ID, session.Options))
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// Save adds a single session to the response.
|
|
|
+func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *gsessions.Session) error {
|
|
|
+ // Marked for deletion.
|
|
|
+ if session.Options.MaxAge < 0 || len(session.Values) == 0 {
|
|
|
+ if err := s.delete(session); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ } else {
|
|
|
+ // Build an alphanumeric key for the redis store.
|
|
|
+ if session.ID == "" {
|
|
|
+ session.ID = shortid.MustGenerate()
|
|
|
+ }
|
|
|
+ if err := s.save(session); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// save stores the session in redis.
|
|
|
+func (s *RediStore) save(session *gsessions.Session) error {
|
|
|
+ b, err := s.serializer.Serialize(session)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if s.maxLength != 0 && len(b) > s.maxLength {
|
|
|
+ return errors.New("SessionStore: the value to store is too big")
|
|
|
+ }
|
|
|
+ age := session.Options.MaxAge
|
|
|
+ if age == 0 {
|
|
|
+ age = s.DefaultMaxAge
|
|
|
+ }
|
|
|
+ _, err = s.RedisClient.Set(s.keyPrefix+session.ID, b, time.Duration(age)*time.Second).Result()
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+// load reads the session from redis.
|
|
|
+// returns true if there is a sessoin data in DB
|
|
|
+func (s *RediStore) load(session *gsessions.Session) (bool, error) {
|
|
|
+ data, err := s.RedisClient.Get(s.keyPrefix + session.ID).Result()
|
|
|
+ if err != nil {
|
|
|
+ return false, err
|
|
|
+ }
|
|
|
+ if data == "" {
|
|
|
+ return false, nil // no data was associated with this key
|
|
|
+ }
|
|
|
+ return true, s.serializer.Deserialize([]byte(data), session)
|
|
|
+}
|
|
|
+
|
|
|
+// delete removes keys from redis if MaxAge<0
|
|
|
+func (s *RediStore) delete(session *gsessions.Session) error {
|
|
|
+ _, err := s.RedisClient.Del(s.keyPrefix + session.ID).Result()
|
|
|
+ return err
|
|
|
+}
|