1// Copyright 2012 The Gorilla Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package sessions
6
7import (
8	"encoding/base32"
9	"io/ioutil"
10	"net/http"
11	"os"
12	"path/filepath"
13	"strings"
14	"sync"
15
16	"github.com/gorilla/securecookie"
17)
18
19// Store is an interface for custom session stores.
20//
21// See CookieStore and FilesystemStore for examples.
22type Store interface {
23	// Get should return a cached session.
24	Get(r *http.Request, name string) (*Session, error)
25
26	// New should create and return a new session.
27	//
28	// Note that New should never return a nil session, even in the case of
29	// an error if using the Registry infrastructure to cache the session.
30	New(r *http.Request, name string) (*Session, error)
31
32	// Save should persist session to the underlying store implementation.
33	Save(r *http.Request, w http.ResponseWriter, s *Session) error
34}
35
36// CookieStore ----------------------------------------------------------------
37
38// NewCookieStore returns a new CookieStore.
39//
40// Keys are defined in pairs to allow key rotation, but the common case is
41// to set a single authentication key and optionally an encryption key.
42//
43// The first key in a pair is used for authentication and the second for
44// encryption. The encryption key can be set to nil or omitted in the last
45// pair, but the authentication key is required in all pairs.
46//
47// It is recommended to use an authentication key with 32 or 64 bytes.
48// The encryption key, if set, must be either 16, 24, or 32 bytes to select
49// AES-128, AES-192, or AES-256 modes.
50func NewCookieStore(keyPairs ...[]byte) *CookieStore {
51	cs := &CookieStore{
52		Codecs: securecookie.CodecsFromPairs(keyPairs...),
53		Options: &Options{
54			Path:   "/",
55			MaxAge: 86400 * 30,
56		},
57	}
58
59	cs.MaxAge(cs.Options.MaxAge)
60	return cs
61}
62
63// CookieStore stores sessions using secure cookies.
64type CookieStore struct {
65	Codecs  []securecookie.Codec
66	Options *Options // default configuration
67}
68
69// Get returns a session for the given name after adding it to the registry.
70//
71// It returns a new session if the sessions doesn't exist. Access IsNew on
72// the session to check if it is an existing session or a new one.
73//
74// It returns a new session and an error if the session exists but could
75// not be decoded.
76func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) {
77	return GetRegistry(r).Get(s, name)
78}
79
80// New returns a session for the given name without adding it to the registry.
81//
82// The difference between New() and Get() is that calling New() twice will
83// decode the session data twice, while Get() registers and reuses the same
84// decoded session after the first call.
85func (s *CookieStore) New(r *http.Request, name string) (*Session, error) {
86	session := NewSession(s, name)
87	opts := *s.Options
88	session.Options = &opts
89	session.IsNew = true
90	var err error
91	if c, errCookie := r.Cookie(name); errCookie == nil {
92		err = securecookie.DecodeMulti(name, c.Value, &session.Values,
93			s.Codecs...)
94		if err == nil {
95			session.IsNew = false
96		}
97	}
98	return session, err
99}
100
101// Save adds a single session to the response.
102func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
103	session *Session) error {
104	encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
105		s.Codecs...)
106	if err != nil {
107		return err
108	}
109	http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
110	return nil
111}
112
113// MaxAge sets the maximum age for the store and the underlying cookie
114// implementation. Individual sessions can be deleted by setting Options.MaxAge
115// = -1 for that session.
116func (s *CookieStore) MaxAge(age int) {
117	s.Options.MaxAge = age
118
119	// Set the maxAge for each securecookie instance.
120	for _, codec := range s.Codecs {
121		if sc, ok := codec.(*securecookie.SecureCookie); ok {
122			sc.MaxAge(age)
123		}
124	}
125}
126
127// FilesystemStore ------------------------------------------------------------
128
129var fileMutex sync.RWMutex
130
131// NewFilesystemStore returns a new FilesystemStore.
132//
133// The path argument is the directory where sessions will be saved. If empty
134// it will use os.TempDir().
135//
136// See NewCookieStore() for a description of the other parameters.
137func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore {
138	if path == "" {
139		path = os.TempDir()
140	}
141	fs := &FilesystemStore{
142		Codecs: securecookie.CodecsFromPairs(keyPairs...),
143		Options: &Options{
144			Path:   "/",
145			MaxAge: 86400 * 30,
146		},
147		path: path,
148	}
149
150	fs.MaxAge(fs.Options.MaxAge)
151	return fs
152}
153
154// FilesystemStore stores sessions in the filesystem.
155//
156// It also serves as a reference for custom stores.
157//
158// This store is still experimental and not well tested. Feedback is welcome.
159type FilesystemStore struct {
160	Codecs  []securecookie.Codec
161	Options *Options // default configuration
162	path    string
163}
164
165// MaxLength restricts the maximum length of new sessions to l.
166// If l is 0 there is no limit to the size of a session, use with caution.
167// The default for a new FilesystemStore is 4096.
168func (s *FilesystemStore) MaxLength(l int) {
169	for _, c := range s.Codecs {
170		if codec, ok := c.(*securecookie.SecureCookie); ok {
171			codec.MaxLength(l)
172		}
173	}
174}
175
176// Get returns a session for the given name after adding it to the registry.
177//
178// See CookieStore.Get().
179func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) {
180	return GetRegistry(r).Get(s, name)
181}
182
183// New returns a session for the given name without adding it to the registry.
184//
185// See CookieStore.New().
186func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) {
187	session := NewSession(s, name)
188	opts := *s.Options
189	session.Options = &opts
190	session.IsNew = true
191	var err error
192	if c, errCookie := r.Cookie(name); errCookie == nil {
193		err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
194		if err == nil {
195			err = s.load(session)
196			if err == nil {
197				session.IsNew = false
198			}
199		}
200	}
201	return session, err
202}
203
204// Save adds a single session to the response.
205//
206// If the Options.MaxAge of the session is <= 0 then the session file will be
207// deleted from the store path. With this process it enforces the properly
208// session cookie handling so no need to trust in the cookie management in the
209// web browser.
210func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter,
211	session *Session) error {
212	// Delete if max-age is <= 0
213	if session.Options.MaxAge <= 0 {
214		if err := s.erase(session); err != nil {
215			return err
216		}
217		http.SetCookie(w, NewCookie(session.Name(), "", session.Options))
218		return nil
219	}
220
221	if session.ID == "" {
222		// Because the ID is used in the filename, encode it to
223		// use alphanumeric characters only.
224		session.ID = strings.TrimRight(
225			base32.StdEncoding.EncodeToString(
226				securecookie.GenerateRandomKey(32)), "=")
227	}
228	if err := s.save(session); err != nil {
229		return err
230	}
231	encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
232		s.Codecs...)
233	if err != nil {
234		return err
235	}
236	http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
237	return nil
238}
239
240// MaxAge sets the maximum age for the store and the underlying cookie
241// implementation. Individual sessions can be deleted by setting Options.MaxAge
242// = -1 for that session.
243func (s *FilesystemStore) MaxAge(age int) {
244	s.Options.MaxAge = age
245
246	// Set the maxAge for each securecookie instance.
247	for _, codec := range s.Codecs {
248		if sc, ok := codec.(*securecookie.SecureCookie); ok {
249			sc.MaxAge(age)
250		}
251	}
252}
253
254// save writes encoded session.Values to a file.
255func (s *FilesystemStore) save(session *Session) error {
256	encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
257		s.Codecs...)
258	if err != nil {
259		return err
260	}
261	filename := filepath.Join(s.path, "session_"+session.ID)
262	fileMutex.Lock()
263	defer fileMutex.Unlock()
264	return ioutil.WriteFile(filename, []byte(encoded), 0600)
265}
266
267// load reads a file and decodes its content into session.Values.
268func (s *FilesystemStore) load(session *Session) error {
269	filename := filepath.Join(s.path, "session_"+session.ID)
270	fileMutex.RLock()
271	defer fileMutex.RUnlock()
272	fdata, err := ioutil.ReadFile(filename)
273	if err != nil {
274		return err
275	}
276	if err = securecookie.DecodeMulti(session.Name(), string(fdata),
277		&session.Values, s.Codecs...); err != nil {
278		return err
279	}
280	return nil
281}
282
283// delete session file
284func (s *FilesystemStore) erase(session *Session) error {
285	filename := filepath.Join(s.path, "session_"+session.ID)
286
287	fileMutex.RLock()
288	defer fileMutex.RUnlock()
289
290	err := os.Remove(filename)
291	return err
292}
293