| // 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 |
| } |