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