1/* 2Package gormstore is a GORM backend for gorilla sessions 3 4Simplest form: 5 6 store := gormstore.New(gorm.Open(...), []byte("secret-hash-key")) 7 8All options: 9 10 store := gormstore.NewOptions( 11 gorm.Open(...), // *gorm.DB 12 gormstore.Options{ 13 TableName: "sessions", // "sessions" is default 14 SkipCreateTable: false, // false is default 15 }, 16 []byte("secret-hash-key"), // 32 or 64 bytes recommended, required 17 []byte("secret-encyption-key")) // nil, 16, 24 or 32 bytes, optional 18 19 // some more settings, see sessions.Options 20 store.SessionOpts.Secure = true 21 store.SessionOpts.HttpOnly = true 22 store.SessionOpts.MaxAge = 60 * 60 * 24 * 60 23 24If you want periodic cleanup of expired sessions: 25 26 quit := make(chan struct{}) 27 go store.PeriodicCleanup(1*time.Hour, quit) 28 29For more information about the keys see https://github.com/gorilla/securecookie 30 31For API to use in HTTP handlers see https://github.com/gorilla/sessions 32*/ 33package gormstore 34 35import ( 36 "encoding/base32" 37 "net/http" 38 "strings" 39 "time" 40 41 "github.com/gorilla/context" 42 "github.com/gorilla/securecookie" 43 "github.com/gorilla/sessions" 44 "github.com/jinzhu/gorm" 45) 46 47const sessionIDLen = 32 48const defaultTableName = "sessions" 49const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days 50const defaultPath = "/" 51 52// Options for gormstore 53type Options struct { 54 TableName string 55 SkipCreateTable bool 56} 57 58// Store represent a gormstore 59type Store struct { 60 db *gorm.DB 61 opts Options 62 Codecs []securecookie.Codec 63 SessionOpts *sessions.Options 64} 65 66type gormSession struct { 67 ID string `sql:"unique_index"` 68 Data string `sql:"type:text"` 69 CreatedAt time.Time 70 UpdatedAt time.Time 71 ExpiresAt time.Time `sql:"index"` 72 73 tableName string `sql:"-"` // just for convenience instead of db.Table(...) 74} 75 76// Define a type for context keys so that they can't clash with anything else stored in context 77type contextKey string 78 79func (gs *gormSession) TableName() string { 80 return gs.tableName 81} 82 83// New creates a new gormstore session 84func New(db *gorm.DB, keyPairs ...[]byte) *Store { 85 return NewOptions(db, Options{}, keyPairs...) 86} 87 88// NewOptions creates a new gormstore session with options 89func NewOptions(db *gorm.DB, opts Options, keyPairs ...[]byte) *Store { 90 st := &Store{ 91 db: db, 92 opts: opts, 93 Codecs: securecookie.CodecsFromPairs(keyPairs...), 94 SessionOpts: &sessions.Options{ 95 Path: defaultPath, 96 MaxAge: defaultMaxAge, 97 }, 98 } 99 if st.opts.TableName == "" { 100 st.opts.TableName = defaultTableName 101 } 102 103 if !st.opts.SkipCreateTable { 104 st.db.AutoMigrate(&gormSession{tableName: st.opts.TableName}) 105 } 106 107 return st 108} 109 110// Get returns a session for the given name after adding it to the registry. 111func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) { 112 return sessions.GetRegistry(r).Get(st, name) 113} 114 115// New creates a session with name without adding it to the registry. 116func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) { 117 session := sessions.NewSession(st, name) 118 opts := *st.SessionOpts 119 session.Options = &opts 120 121 st.MaxAge(st.SessionOpts.MaxAge) 122 123 // try fetch from db if there is a cookie 124 if cookie, err := r.Cookie(name); err == nil { 125 if err := securecookie.DecodeMulti(name, cookie.Value, &session.ID, st.Codecs...); err != nil { 126 return session, nil 127 } 128 s := &gormSession{tableName: st.opts.TableName} 129 if err := st.db.Where("id = ? AND expires_at > ?", session.ID, gorm.NowFunc()).First(s).Error; err != nil { 130 return session, nil 131 } 132 if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil { 133 return session, nil 134 } 135 136 context.Set(r, contextKey(name), s) 137 } 138 139 return session, nil 140} 141 142// Save session and set cookie header 143func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { 144 s, _ := context.Get(r, contextKey(session.Name())).(*gormSession) 145 146 // delete if max age is < 0 147 if session.Options.MaxAge < 0 { 148 if s != nil { 149 if err := st.db.Delete(s).Error; err != nil { 150 return err 151 } 152 } 153 http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) 154 return nil 155 } 156 157 data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...) 158 if err != nil { 159 return err 160 } 161 now := time.Now() 162 expire := now.Add(time.Second * time.Duration(session.Options.MaxAge)) 163 164 if s == nil { 165 // generate random session ID key suitable for storage in the db 166 session.ID = strings.TrimRight( 167 base32.StdEncoding.EncodeToString( 168 securecookie.GenerateRandomKey(sessionIDLen)), "=") 169 s = &gormSession{ 170 ID: session.ID, 171 Data: data, 172 CreatedAt: now, 173 UpdatedAt: now, 174 ExpiresAt: expire, 175 tableName: st.opts.TableName, 176 } 177 if err := st.db.Create(s).Error; err != nil { 178 return err 179 } 180 context.Set(r, contextKey(session.Name()), s) 181 } else { 182 s.Data = data 183 s.UpdatedAt = now 184 s.ExpiresAt = expire 185 if err := st.db.Save(s).Error; err != nil { 186 return err 187 } 188 } 189 190 // set session id cookie 191 id, err := securecookie.EncodeMulti(session.Name(), session.ID, st.Codecs...) 192 if err != nil { 193 return err 194 } 195 http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options)) 196 197 return nil 198} 199 200// MaxAge sets the maximum age for the store and the underlying cookie 201// implementation. Individual sessions can be deleted by setting 202// Options.MaxAge = -1 for that session. 203func (st *Store) MaxAge(age int) { 204 st.SessionOpts.MaxAge = age 205 for _, codec := range st.Codecs { 206 if sc, ok := codec.(*securecookie.SecureCookie); ok { 207 sc.MaxAge(age) 208 } 209 } 210} 211 212// MaxLength restricts the maximum length of new sessions to l. 213// If l is 0 there is no limit to the size of a session, use with caution. 214// The default is 4096 (default for securecookie) 215func (st *Store) MaxLength(l int) { 216 for _, c := range st.Codecs { 217 if codec, ok := c.(*securecookie.SecureCookie); ok { 218 codec.MaxLength(l) 219 } 220 } 221} 222 223// Cleanup deletes expired sessions 224func (st *Store) Cleanup() { 225 st.db.Delete(&gormSession{tableName: st.opts.TableName}, "expires_at <= ?", gorm.NowFunc()) 226} 227 228// PeriodicCleanup runs Cleanup every interval. Close quit channel to stop. 229func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) { 230 t := time.NewTicker(interval) 231 defer t.Stop() 232 for { 233 select { 234 case <-t.C: 235 st.Cleanup() 236 case <-quit: 237 return 238 } 239 } 240} 241