You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
292 lines
8.4 KiB
292 lines
8.4 KiB
// Copyright 2012 The Gorilla Authors. All rights reserved. |
|
// Use of this source code is governed by a BSD-style |
|
// license that can be found in the LICENSE file. |
|
|
|
package sessions |
|
|
|
import ( |
|
"encoding/base32" |
|
"io/ioutil" |
|
"net/http" |
|
"os" |
|
"path/filepath" |
|
"strings" |
|
"sync" |
|
|
|
"github.com/gorilla/securecookie" |
|
) |
|
|
|
// Store is an interface for custom session stores. |
|
// |
|
// See CookieStore and FilesystemStore for examples. |
|
type Store interface { |
|
// Get should return a cached session. |
|
Get(r *http.Request, name string) (*Session, error) |
|
|
|
// New should create and return a new session. |
|
// |
|
// Note that New should never return a nil session, even in the case of |
|
// an error if using the Registry infrastructure to cache the session. |
|
New(r *http.Request, name string) (*Session, error) |
|
|
|
// Save should persist session to the underlying store implementation. |
|
Save(r *http.Request, w http.ResponseWriter, s *Session) error |
|
} |
|
|
|
// CookieStore ---------------------------------------------------------------- |
|
|
|
// NewCookieStore returns a new CookieStore. |
|
// |
|
// Keys are defined in pairs to allow key rotation, but the common case is |
|
// to set a single authentication key and optionally an encryption key. |
|
// |
|
// The first key in a pair is used for authentication and the second for |
|
// encryption. The encryption key can be set to nil or omitted in the last |
|
// pair, but the authentication key is required in all pairs. |
|
// |
|
// It is recommended to use an authentication key with 32 or 64 bytes. |
|
// The encryption key, if set, must be either 16, 24, or 32 bytes to select |
|
// AES-128, AES-192, or AES-256 modes. |
|
func NewCookieStore(keyPairs ...[]byte) *CookieStore { |
|
cs := &CookieStore{ |
|
Codecs: securecookie.CodecsFromPairs(keyPairs...), |
|
Options: &Options{ |
|
Path: "/", |
|
MaxAge: 86400 * 30, |
|
}, |
|
} |
|
|
|
cs.MaxAge(cs.Options.MaxAge) |
|
return cs |
|
} |
|
|
|
// CookieStore stores sessions using secure cookies. |
|
type CookieStore struct { |
|
Codecs []securecookie.Codec |
|
Options *Options // default configuration |
|
} |
|
|
|
// Get returns a session for the given name after adding it to the registry. |
|
// |
|
// It returns a new session if the sessions doesn't exist. Access IsNew on |
|
// the session to check if it is an existing session or a new one. |
|
// |
|
// It returns a new session and an error if the session exists but could |
|
// not be decoded. |
|
func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { |
|
return GetRegistry(r).Get(s, name) |
|
} |
|
|
|
// New returns a session for the given name without adding it to the registry. |
|
// |
|
// The difference between New() and Get() is that calling New() twice will |
|
// decode the session data twice, while Get() registers and reuses the same |
|
// decoded session after the first call. |
|
func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { |
|
session := NewSession(s, name) |
|
opts := *s.Options |
|
session.Options = &opts |
|
session.IsNew = true |
|
var err error |
|
if c, errCookie := r.Cookie(name); errCookie == nil { |
|
err = securecookie.DecodeMulti(name, c.Value, &session.Values, |
|
s.Codecs...) |
|
if err == nil { |
|
session.IsNew = false |
|
} |
|
} |
|
return session, err |
|
} |
|
|
|
// Save adds a single session to the response. |
|
func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, |
|
session *Session) error { |
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, |
|
s.Codecs...) |
|
if err != nil { |
|
return err |
|
} |
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) |
|
return nil |
|
} |
|
|
|
// MaxAge sets the maximum age for the store and the underlying cookie |
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge |
|
// = -1 for that session. |
|
func (s *CookieStore) MaxAge(age int) { |
|
s.Options.MaxAge = age |
|
|
|
// Set the maxAge for each securecookie instance. |
|
for _, codec := range s.Codecs { |
|
if sc, ok := codec.(*securecookie.SecureCookie); ok { |
|
sc.MaxAge(age) |
|
} |
|
} |
|
} |
|
|
|
// FilesystemStore ------------------------------------------------------------ |
|
|
|
var fileMutex sync.RWMutex |
|
|
|
// NewFilesystemStore returns a new FilesystemStore. |
|
// |
|
// The path argument is the directory where sessions will be saved. If empty |
|
// it will use os.TempDir(). |
|
// |
|
// See NewCookieStore() for a description of the other parameters. |
|
func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore { |
|
if path == "" { |
|
path = os.TempDir() |
|
} |
|
fs := &FilesystemStore{ |
|
Codecs: securecookie.CodecsFromPairs(keyPairs...), |
|
Options: &Options{ |
|
Path: "/", |
|
MaxAge: 86400 * 30, |
|
}, |
|
path: path, |
|
} |
|
|
|
fs.MaxAge(fs.Options.MaxAge) |
|
return fs |
|
} |
|
|
|
// FilesystemStore stores sessions in the filesystem. |
|
// |
|
// It also serves as a reference for custom stores. |
|
// |
|
// This store is still experimental and not well tested. Feedback is welcome. |
|
type FilesystemStore struct { |
|
Codecs []securecookie.Codec |
|
Options *Options // default configuration |
|
path string |
|
} |
|
|
|
// 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 FilesystemStore is 4096. |
|
func (s *FilesystemStore) MaxLength(l int) { |
|
for _, c := range s.Codecs { |
|
if codec, ok := c.(*securecookie.SecureCookie); ok { |
|
codec.MaxLength(l) |
|
} |
|
} |
|
} |
|
|
|
// Get returns a session for the given name after adding it to the registry. |
|
// |
|
// See CookieStore.Get(). |
|
func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { |
|
return GetRegistry(r).Get(s, name) |
|
} |
|
|
|
// New returns a session for the given name without adding it to the registry. |
|
// |
|
// See CookieStore.New(). |
|
func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) { |
|
session := NewSession(s, name) |
|
opts := *s.Options |
|
session.Options = &opts |
|
session.IsNew = true |
|
var err error |
|
if c, errCookie := r.Cookie(name); errCookie == nil { |
|
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) |
|
if err == nil { |
|
err = s.load(session) |
|
if err == nil { |
|
session.IsNew = false |
|
} |
|
} |
|
} |
|
return session, err |
|
} |
|
|
|
// Save adds a single session to the response. |
|
// |
|
// If the Options.MaxAge of the session is <= 0 then the session file will be |
|
// deleted from the store path. With this process it enforces the properly |
|
// session cookie handling so no need to trust in the cookie management in the |
|
// web browser. |
|
func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter, |
|
session *Session) error { |
|
// Delete if max-age is <= 0 |
|
if session.Options.MaxAge <= 0 { |
|
if err := s.erase(session); err != nil { |
|
return err |
|
} |
|
http.SetCookie(w, NewCookie(session.Name(), "", session.Options)) |
|
return nil |
|
} |
|
|
|
if session.ID == "" { |
|
// Because the ID is used in the filename, encode it to |
|
// use alphanumeric characters only. |
|
session.ID = strings.TrimRight( |
|
base32.StdEncoding.EncodeToString( |
|
securecookie.GenerateRandomKey(32)), "=") |
|
} |
|
if err := s.save(session); err != nil { |
|
return err |
|
} |
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, |
|
s.Codecs...) |
|
if err != nil { |
|
return err |
|
} |
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options)) |
|
return nil |
|
} |
|
|
|
// MaxAge sets the maximum age for the store and the underlying cookie |
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge |
|
// = -1 for that session. |
|
func (s *FilesystemStore) MaxAge(age int) { |
|
s.Options.MaxAge = age |
|
|
|
// Set the maxAge for each securecookie instance. |
|
for _, codec := range s.Codecs { |
|
if sc, ok := codec.(*securecookie.SecureCookie); ok { |
|
sc.MaxAge(age) |
|
} |
|
} |
|
} |
|
|
|
// save writes encoded session.Values to a file. |
|
func (s *FilesystemStore) save(session *Session) error { |
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, |
|
s.Codecs...) |
|
if err != nil { |
|
return err |
|
} |
|
filename := filepath.Join(s.path, "session_"+session.ID) |
|
fileMutex.Lock() |
|
defer fileMutex.Unlock() |
|
return ioutil.WriteFile(filename, []byte(encoded), 0600) |
|
} |
|
|
|
// load reads a file and decodes its content into session.Values. |
|
func (s *FilesystemStore) load(session *Session) error { |
|
filename := filepath.Join(s.path, "session_"+session.ID) |
|
fileMutex.RLock() |
|
defer fileMutex.RUnlock() |
|
fdata, err := ioutil.ReadFile(filename) |
|
if err != nil { |
|
return err |
|
} |
|
if err = securecookie.DecodeMulti(session.Name(), string(fdata), |
|
&session.Values, s.Codecs...); err != nil { |
|
return err |
|
} |
|
return nil |
|
} |
|
|
|
// delete session file |
|
func (s *FilesystemStore) erase(session *Session) error { |
|
filename := filepath.Join(s.path, "session_"+session.ID) |
|
|
|
fileMutex.RLock() |
|
defer fileMutex.RUnlock() |
|
|
|
err := os.Remove(filename) |
|
return err |
|
}
|
|
|