337 lines
9.2 KiB
Go
337 lines
9.2 KiB
Go
// 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"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/admpub/securecookie"
|
|
"github.com/webx-top/echo"
|
|
)
|
|
|
|
// Store is an interface for custom session stores.
|
|
//
|
|
// See CookieStore and FilesystemStore for examples.
|
|
type Store interface {
|
|
// Get should return a cached session.
|
|
Get(ctx echo.Context, 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(ctx echo.Context, name string) (*Session, error)
|
|
Reload(ctx echo.Context, s *Session) error
|
|
|
|
// Save should persist session to the underlying store implementation.
|
|
Save(ctx echo.Context, s *Session) error
|
|
// Remove server-side data
|
|
Remove(sessionID string) error
|
|
}
|
|
|
|
// IDGenerator session id generator
|
|
type IDGenerator interface {
|
|
GenerateID(ctx echo.Context, session *Session) (string, 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.
|
|
//
|
|
// Use the convenience function securecookie.GenerateRandomKey() to create
|
|
// strong keys.
|
|
func NewCookieStore(keyPairs ...[]byte) *CookieStore {
|
|
cs := &CookieStore{
|
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
|
}
|
|
return cs
|
|
}
|
|
|
|
// CookieStore stores sessions using secure cookies.
|
|
type CookieStore struct {
|
|
Codecs []securecookie.Codec
|
|
}
|
|
|
|
// 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(ctx echo.Context, name string) (*Session, error) {
|
|
return GetRegistry(ctx).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(ctx echo.Context, name string) (*Session, error) {
|
|
session := NewSession(s, name)
|
|
session.IsNew = true
|
|
var err error
|
|
if v := ctx.GetCookie(name); len(v) > 0 {
|
|
err = securecookie.DecodeMultiWithMaxAge(
|
|
name, v, &session.Values,
|
|
ctx.CookieOptions().MaxAge,
|
|
s.Codecs...)
|
|
if err == nil {
|
|
session.IsNew = false
|
|
session.ID = v
|
|
}
|
|
}
|
|
return session, err
|
|
}
|
|
|
|
func (s *CookieStore) Reload(ctx echo.Context, session *Session) error {
|
|
if len(session.ID) == 0 {
|
|
return nil
|
|
}
|
|
err := securecookie.DecodeMultiWithMaxAge(
|
|
session.Name(), session.ID, &session.Values,
|
|
ctx.CookieOptions().MaxAge,
|
|
s.Codecs...)
|
|
if err == nil {
|
|
session.IsNew = false
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *CookieStore) GenerateID(ctx echo.Context, session *Session) (string, error) {
|
|
return securecookie.EncodeMulti(session.Name(), session.Values,
|
|
s.Codecs...)
|
|
}
|
|
|
|
// Save adds a single session to the response.
|
|
func (s *CookieStore) Save(ctx echo.Context, session *Session) error {
|
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
|
s.Codecs...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
SetCookie(ctx, session.Name(), encoded)
|
|
return nil
|
|
}
|
|
|
|
func (s *CookieStore) Remove(sessionID string) error {
|
|
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) {
|
|
// 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 len(path) == 0 {
|
|
path = os.TempDir()
|
|
}
|
|
fs := &FilesystemStore{
|
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
|
path: path,
|
|
}
|
|
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
|
|
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(ctx echo.Context, name string) (*Session, error) {
|
|
return GetRegistry(ctx).Get(s, name)
|
|
}
|
|
|
|
// New returns a session for the given name without adding it to the registry.
|
|
//
|
|
// See CookieStore.New().
|
|
func (s *FilesystemStore) New(ctx echo.Context, name string) (*Session, error) {
|
|
session := NewSession(s, name)
|
|
session.IsNew = true
|
|
var err error
|
|
if v := ctx.GetCookie(name); len(v) > 0 {
|
|
err = securecookie.DecodeMultiWithMaxAge(
|
|
name, v, &session.ID,
|
|
ctx.CookieOptions().MaxAge,
|
|
s.Codecs...)
|
|
if err == nil {
|
|
err = s.load(ctx, session)
|
|
if err == nil {
|
|
session.IsNew = false
|
|
}
|
|
}
|
|
}
|
|
return session, err
|
|
}
|
|
|
|
func (s *FilesystemStore) Reload(ctx echo.Context, session *Session) error {
|
|
err := s.load(ctx, session)
|
|
if err == nil {
|
|
session.IsNew = false
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Save adds a single session to the response.
|
|
func (s *FilesystemStore) Save(ctx echo.Context, session *Session) error {
|
|
// Delete if max-age is < 0
|
|
if ctx.CookieOptions().MaxAge < 0 {
|
|
if err := s.erase(session); err != nil {
|
|
return err
|
|
}
|
|
SetCookie(ctx, session.Name(), "", -1)
|
|
return nil
|
|
}
|
|
if len(session.ID) == 0 {
|
|
// 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
|
|
}
|
|
SetCookie(ctx, session.Name(), encoded)
|
|
return nil
|
|
}
|
|
|
|
func (s *FilesystemStore) Remove(sessionID string) error {
|
|
if len(sessionID) == 0 {
|
|
return nil
|
|
}
|
|
filename := filepath.Join(s.path, "session_"+sessionID)
|
|
fileMutex.RLock()
|
|
defer fileMutex.RUnlock()
|
|
|
|
err := os.Remove(filename)
|
|
if err != nil && os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// delete session file
|
|
func (s *FilesystemStore) erase(session *Session) error {
|
|
return s.Remove(session.ID)
|
|
}
|
|
|
|
// 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) {
|
|
// 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 {
|
|
b, err := securecookie.Gob.Serialize(session.Values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filename := filepath.Join(s.path, "session_"+session.ID)
|
|
fileMutex.Lock()
|
|
defer fileMutex.Unlock()
|
|
return os.WriteFile(filename, b, 0600)
|
|
}
|
|
|
|
// load reads a file and decodes its content into session.Values.
|
|
func (s *FilesystemStore) load(ctx echo.Context, session *Session) error {
|
|
filename := filepath.Join(s.path, "session_"+session.ID)
|
|
fileMutex.RLock()
|
|
defer fileMutex.RUnlock()
|
|
fdata, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return securecookie.Gob.Deserialize(fdata, &session.Values)
|
|
}
|
|
|
|
func (s *FilesystemStore) DeleteExpired(maxAge float64) error {
|
|
if maxAge <= 0 {
|
|
return nil
|
|
}
|
|
err := filepath.Walk(s.path, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if info.IsDir() {
|
|
return err
|
|
}
|
|
if !strings.HasPrefix(info.Name(), `session_`) {
|
|
return err
|
|
}
|
|
if time.Since(info.ModTime()).Seconds() > maxAge {
|
|
err = os.Remove(path)
|
|
}
|
|
return err
|
|
})
|
|
return err
|
|
}
|