2019-04-28 07:30:35 +02:00
package scs
import (
"context"
"crypto/rand"
"encoding/base64"
"fmt"
"sort"
"sync"
2019-11-27 17:43:05 +02:00
"sync/atomic"
2019-04-28 07:30:35 +02:00
"time"
)
// Status represents the state of the session data during a request cycle.
type Status int
const (
// Unmodified indicates that the session data hasn't been changed in the
// current request cycle.
Unmodified Status = iota
// Modified indicates that the session data has been changed in the current
// request cycle.
Modified
// Destroyed indicates that the session data has been destroyed in the
// current request cycle.
Destroyed
)
type sessionData struct {
2019-09-03 18:34:13 +02:00
deadline time . Time
2019-04-28 07:30:35 +02:00
status Status
token string
2019-09-03 18:34:13 +02:00
values map [ string ] interface { }
2019-04-28 07:30:35 +02:00
mu sync . Mutex
}
func newSessionData ( lifetime time . Duration ) * sessionData {
return & sessionData {
2019-09-03 18:34:13 +02:00
deadline : time . Now ( ) . Add ( lifetime ) . UTC ( ) ,
2019-04-28 07:30:35 +02:00
status : Unmodified ,
2019-09-03 18:34:13 +02:00
values : make ( map [ string ] interface { } ) ,
2019-04-28 07:30:35 +02:00
}
}
// Load retrieves the session data for the given token from the session store,
// and returns a new context.Context containing the session data. If no matching
// token is found then this will create a new session.
//
// Most applications will use the LoadAndSave() middleware and will not need to
// use this method.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Load ( ctx context . Context , token string ) ( context . Context , error ) {
2019-04-28 07:30:35 +02:00
if _ , ok := ctx . Value ( s . contextKey ) . ( * sessionData ) ; ok {
return ctx , nil
}
if token == "" {
return s . addSessionDataToContext ( ctx , newSessionData ( s . Lifetime ) ) , nil
}
2021-10-23 15:23:43 +08:00
b , found , err := s . doStoreFind ( ctx , token )
2019-04-28 07:30:35 +02:00
if err != nil {
return nil , err
} else if ! found {
return s . addSessionDataToContext ( ctx , newSessionData ( s . Lifetime ) ) , nil
}
sd := & sessionData {
status : Unmodified ,
token : token ,
}
2020-07-20 01:15:20 -05:00
if sd . deadline , sd . values , err = s . Codec . Decode ( b ) ; err != nil {
2019-04-28 07:30:35 +02:00
return nil , err
}
2020-07-20 01:15:20 -05:00
2019-04-28 07:30:35 +02:00
// Mark the session data as modified if an idle timeout is being used. This
// will force the session data to be re-committed to the session store with
// a new expiry time.
if s . IdleTimeout > 0 {
sd . status = Modified
}
return s . addSessionDataToContext ( ctx , sd ) , nil
}
// Commit saves the session data to the session store and returns the session
// token and expiry time.
//
// Most applications will use the LoadAndSave() middleware and will not need to
// use this method.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Commit ( ctx context . Context ) ( string , time . Time , error ) {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
if sd . token == "" {
var err error
2020-07-20 01:15:20 -05:00
if sd . token , err = generateToken ( ) ; err != nil {
2019-04-28 07:30:35 +02:00
return "" , time . Time { } , err
}
}
2019-09-03 18:34:13 +02:00
b , err := s . Codec . Encode ( sd . deadline , sd . values )
2019-04-28 07:30:35 +02:00
if err != nil {
return "" , time . Time { } , err
}
2019-09-03 18:34:13 +02:00
expiry := sd . deadline
2019-04-28 07:30:35 +02:00
if s . IdleTimeout > 0 {
2019-11-26 14:19:14 +02:00
ie := time . Now ( ) . Add ( s . IdleTimeout ) . UTC ( )
2019-04-28 07:30:35 +02:00
if ie . Before ( expiry ) {
expiry = ie
}
}
2021-10-23 15:23:43 +08:00
if err := s . doStoreCommit ( ctx , sd . token , b , expiry ) ; err != nil {
2019-04-28 07:30:35 +02:00
return "" , time . Time { } , err
}
return sd . token , expiry , nil
}
// Destroy deletes the session data from the session store and sets the session
2019-05-04 19:17:39 +02:00
// status to Destroyed. Any further operations in the same request cycle will
2019-04-28 07:30:35 +02:00
// result in a new session being created.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Destroy ( ctx context . Context ) error {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2021-10-23 15:23:43 +08:00
err := s . doStoreDelete ( ctx , sd . token )
2019-04-28 07:30:35 +02:00
if err != nil {
return err
}
sd . status = Destroyed
// Reset everything else to defaults.
sd . token = ""
2019-09-03 18:34:13 +02:00
sd . deadline = time . Now ( ) . Add ( s . Lifetime ) . UTC ( )
for key := range sd . values {
delete ( sd . values , key )
2019-04-28 07:30:35 +02:00
}
return nil
}
// Put adds a key and corresponding value to the session data. Any existing
// value for the key will be replaced. The session data status will be set to
// Modified.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Put ( ctx context . Context , key string , val interface { } ) {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
2019-09-03 18:34:13 +02:00
sd . values [ key ] = val
2019-04-28 07:30:35 +02:00
sd . status = Modified
sd . mu . Unlock ( )
}
// Get returns the value for a given key from the session data. The return
// value has the type interface{} so will usually need to be type asserted
// before you can use it. For example:
//
// foo, ok := session.Get(r, "foo").(string)
// if !ok {
// return errors.New("type assertion to string failed")
// }
//
// Also see the GetString(), GetInt(), GetBytes() and other helper methods which
// wrap the type conversion for common types.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Get ( ctx context . Context , key string ) interface { } {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
return sd . values [ key ]
2019-04-28 07:30:35 +02:00
}
// Pop acts like a one-time Get. It returns the value for a given key from the
// session data and deletes the key and value from the session data. The
// session data status will be set to Modified. The return value has the type
// interface{} so will usually need to be type asserted before you can use it.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Pop ( ctx context . Context , key string ) interface { } {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
val , exists := sd . values [ key ]
2019-04-28 07:30:35 +02:00
if ! exists {
return nil
}
2019-09-03 18:34:13 +02:00
delete ( sd . values , key )
2019-04-28 07:30:35 +02:00
sd . status = Modified
return val
}
// Remove deletes the given key and corresponding value from the session data.
// The session data status will be set to Modified. If the key is not present
// this operation is a no-op.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Remove ( ctx context . Context , key string ) {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
_ , exists := sd . values [ key ]
2019-04-28 07:30:35 +02:00
if ! exists {
return
}
2019-09-03 18:34:13 +02:00
delete ( sd . values , key )
2019-04-28 07:30:35 +02:00
sd . status = Modified
}
2019-05-27 14:34:27 +02:00
// Clear removes all data for the current session. The session token and
// lifetime are unaffected. If there is no data in the current session this is
// a no-op.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Clear ( ctx context . Context ) error {
2019-05-27 14:34:27 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2019-09-03 18:34:13 +02:00
if len ( sd . values ) == 0 {
2019-05-27 14:34:27 +02:00
return nil
}
2019-09-03 18:34:13 +02:00
for key := range sd . values {
delete ( sd . values , key )
2019-05-27 14:34:27 +02:00
}
sd . status = Modified
return nil
}
2019-04-28 07:30:35 +02:00
// Exists returns true if the given key is present in the session data.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Exists ( ctx context . Context , key string ) bool {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
2019-09-03 18:34:13 +02:00
_ , exists := sd . values [ key ]
2019-04-28 07:30:35 +02:00
sd . mu . Unlock ( )
return exists
}
// Keys returns a slice of all key names present in the session data, sorted
// alphabetically. If the data contains no data then an empty slice will be
// returned.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Keys ( ctx context . Context ) [ ] string {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
2019-09-03 18:34:13 +02:00
keys := make ( [ ] string , len ( sd . values ) )
2019-04-28 07:30:35 +02:00
i := 0
2019-09-03 18:34:13 +02:00
for key := range sd . values {
2019-04-28 07:30:35 +02:00
keys [ i ] = key
i ++
}
sd . mu . Unlock ( )
sort . Strings ( keys )
return keys
}
// RenewToken updates the session data to have a new session token while
// retaining the current session data. The session lifetime is also reset and
// the session data status will be set to Modified.
//
// The old session token and accompanying data are deleted from the session store.
//
// To mitigate the risk of session fixation attacks, it's important that you call
// RenewToken before making any changes to privilege levels (e.g. login and
// logout operations). See https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Session_Management_Cheat_Sheet.md#renew-the-session-id-after-any-privilege-level-change
// for additional information.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) RenewToken ( ctx context . Context ) error {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
2021-10-23 15:23:43 +08:00
err := s . doStoreDelete ( ctx , sd . token )
2019-04-28 07:30:35 +02:00
if err != nil {
return err
}
newToken , err := generateToken ( )
if err != nil {
return err
}
sd . token = newToken
2019-09-03 18:34:13 +02:00
sd . deadline = time . Now ( ) . Add ( s . Lifetime ) . UTC ( )
2019-04-28 07:30:35 +02:00
sd . status = Modified
return nil
}
// Status returns the current status of the session data.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) Status ( ctx context . Context ) Status {
2019-04-28 07:30:35 +02:00
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
return sd . status
}
// GetString returns the string value for a given key from the session data.
// The zero value for a string ("") is returned if the key does not exist or the
// value could not be type asserted to a string.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetString ( ctx context . Context , key string ) string {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
str , ok := val . ( string )
if ! ok {
return ""
}
return str
}
// GetBool returns the bool value for a given key from the session data. The
// zero value for a bool (false) is returned if the key does not exist or the
// value could not be type asserted to a bool.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetBool ( ctx context . Context , key string ) bool {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
b , ok := val . ( bool )
if ! ok {
return false
}
return b
}
// GetInt returns the int value for a given key from the session data. The
// zero value for an int (0) is returned if the key does not exist or the
// value could not be type asserted to an int.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetInt ( ctx context . Context , key string ) int {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
i , ok := val . ( int )
if ! ok {
return 0
}
return i
}
2021-08-03 11:17:04 +02:00
// GetInt64 returns the int64 value for a given key from the session data. The
// zero value for an int64 (0) is returned if the key does not exist or the
// value could not be type asserted to an int64.
func ( s * SessionManager ) GetInt64 ( ctx context . Context , key string ) int64 {
val := s . Get ( ctx , key )
i , ok := val . ( int64 )
if ! ok {
return 0
}
return i
}
// GetInt32 returns the int value for a given key from the session data. The
// zero value for an int32 (0) is returned if the key does not exist or the
// value could not be type asserted to an int32.
func ( s * SessionManager ) GetInt32 ( ctx context . Context , key string ) int32 {
val := s . Get ( ctx , key )
i , ok := val . ( int32 )
if ! ok {
return 0
}
return i
}
2019-04-28 07:30:35 +02:00
// GetFloat returns the float64 value for a given key from the session data. The
// zero value for an float64 (0) is returned if the key does not exist or the
// value could not be type asserted to a float64.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetFloat ( ctx context . Context , key string ) float64 {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
f , ok := val . ( float64 )
if ! ok {
return 0
}
return f
}
// GetBytes returns the byte slice ([]byte) value for a given key from the session
// data. The zero value for a slice (nil) is returned if the key does not exist
// or could not be type asserted to []byte.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetBytes ( ctx context . Context , key string ) [ ] byte {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
b , ok := val . ( [ ] byte )
if ! ok {
return nil
}
return b
}
// GetTime returns the time.Time value for a given key from the session data. The
// zero value for a time.Time object is returned if the key does not exist or the
// value could not be type asserted to a time.Time. This can be tested with the
// time.IsZero() method.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) GetTime ( ctx context . Context , key string ) time . Time {
2019-04-28 07:30:35 +02:00
val := s . Get ( ctx , key )
t , ok := val . ( time . Time )
if ! ok {
return time . Time { }
}
return t
}
// PopString returns the string value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for a string ("") is returned if the key does not exist or the value
// could not be type asserted to a string.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopString ( ctx context . Context , key string ) string {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
str , ok := val . ( string )
if ! ok {
return ""
}
return str
}
// PopBool returns the bool value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for a bool (false) is returned if the key does not exist or the value
// could not be type asserted to a bool.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopBool ( ctx context . Context , key string ) bool {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
b , ok := val . ( bool )
if ! ok {
return false
}
return b
}
// PopInt returns the int value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for an int (0) is returned if the key does not exist or the value could
// not be type asserted to an int.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopInt ( ctx context . Context , key string ) int {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
i , ok := val . ( int )
if ! ok {
return 0
}
return i
}
// PopFloat returns the float64 value for a given key and then deletes it from the
// session data. The session data status will be set to Modified. The zero
// value for an float64 (0) is returned if the key does not exist or the value
// could not be type asserted to a float64.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopFloat ( ctx context . Context , key string ) float64 {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
f , ok := val . ( float64 )
if ! ok {
return 0
}
return f
}
// PopBytes returns the byte slice ([]byte) value for a given key and then
// deletes it from the from the session data. The session data status will be
// set to Modified. The zero value for a slice (nil) is returned if the key does
// not exist or could not be type asserted to []byte.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopBytes ( ctx context . Context , key string ) [ ] byte {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
b , ok := val . ( [ ] byte )
if ! ok {
return nil
}
return b
}
// PopTime returns the time.Time value for a given key and then deletes it from
// the session data. The session data status will be set to Modified. The zero
// value for a time.Time object is returned if the key does not exist or the
// value could not be type asserted to a time.Time.
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) PopTime ( ctx context . Context , key string ) time . Time {
2019-04-28 07:30:35 +02:00
val := s . Pop ( ctx , key )
t , ok := val . ( time . Time )
if ! ok {
return time . Time { }
}
return t
}
2020-11-22 16:57:47 +01:00
// RememberMe controls whether the session cookie is persistent (i.e whether it
// is retained after a user closes their browser). RememberMe only has an effect
// if you have set SessionManager.Cookie.Persist = false (the default is true) and
// you are using the standard LoadAndSave() middleware.
func ( s * SessionManager ) RememberMe ( ctx context . Context , val bool ) {
s . Put ( ctx , "__rememberMe" , val )
}
2021-08-22 18:08:19 +02:00
// Iterate retrieves all active (i.e. not expired) sessions from the store and
// executes the provided function fn for each session. If the session store
// being used does not support iteration then Iterate will panic.
2021-11-26 13:00:03 +01:00
func ( s * SessionManager ) Iterate ( ctx context . Context , fn func ( context . Context ) error ) error {
allSessions , err := s . doStoreAll ( ctx )
2021-08-22 18:08:19 +02:00
if err != nil {
return err
}
for token , b := range allSessions {
sd := & sessionData {
status : Unmodified ,
token : token ,
}
sd . deadline , sd . values , err = s . Codec . Decode ( b )
if err != nil {
return err
}
ctx = s . addSessionDataToContext ( ctx , sd )
err = fn ( ctx )
if err != nil {
return err
}
}
return nil
}
2021-08-22 18:40:20 +02:00
// Deadline returns the 'absolute' expiry time for the session. Please note
// that if you are using an idle timeout, it is possible that a session will
// expire due to non-use before the returned deadline.
func ( s * SessionManager ) Deadline ( ctx context . Context ) time . Time {
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
return sd . deadline
}
2021-10-27 17:49:28 +02:00
// Token returns the session token. Please note that this will return the
// empty string "" if it is called before the session has been committed to
// the store.
func ( s * SessionManager ) Token ( ctx context . Context ) string {
sd := s . getSessionDataFromContext ( ctx )
sd . mu . Lock ( )
defer sd . mu . Unlock ( )
return sd . token
}
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) addSessionDataToContext ( ctx context . Context , sd * sessionData ) context . Context {
2019-04-28 07:30:35 +02:00
return context . WithValue ( ctx , s . contextKey , sd )
}
2019-06-18 12:49:37 +02:00
func ( s * SessionManager ) getSessionDataFromContext ( ctx context . Context ) * sessionData {
2019-04-28 07:30:35 +02:00
c , ok := ctx . Value ( s . contextKey ) . ( * sessionData )
if ! ok {
panic ( "scs: no session data in context" )
}
return c
}
func generateToken ( ) ( string , error ) {
b := make ( [ ] byte , 32 )
_ , err := rand . Read ( b )
if err != nil {
return "" , err
}
return base64 . RawURLEncoding . EncodeToString ( b ) , nil
}
type contextKey string
2020-05-26 01:32:29 -05:00
var (
contextKeyID uint64
contextKeyIDMutex = & sync . Mutex { }
)
2019-04-28 07:30:35 +02:00
func generateContextKey ( ) contextKey {
2020-05-26 01:32:29 -05:00
contextKeyIDMutex . Lock ( )
defer contextKeyIDMutex . Unlock ( )
2019-11-27 17:43:05 +02:00
atomic . AddUint64 ( & contextKeyID , 1 )
2019-04-28 07:30:35 +02:00
return contextKey ( fmt . Sprintf ( "session.%d" , contextKeyID ) )
}
2021-10-23 15:23:43 +08:00
func ( s * SessionManager ) doStoreDelete ( ctx context . Context , token string ) ( err error ) {
c , ok := s . Store . ( interface {
DeleteCtx ( context . Context , string ) error
} )
if ok {
return c . DeleteCtx ( ctx , token )
}
return s . Store . Delete ( token )
}
func ( s * SessionManager ) doStoreFind ( ctx context . Context , token string ) ( b [ ] byte , found bool , err error ) {
c , ok := s . Store . ( interface {
FindCtx ( context . Context , string ) ( [ ] byte , bool , error )
} )
if ok {
return c . FindCtx ( ctx , token )
}
return s . Store . Find ( token )
}
func ( s * SessionManager ) doStoreCommit ( ctx context . Context , token string , b [ ] byte , expiry time . Time ) ( err error ) {
c , ok := s . Store . ( interface {
CommitCtx ( context . Context , string , [ ] byte , time . Time ) error
} )
if ok {
return c . CommitCtx ( ctx , token , b , expiry )
}
return s . Store . Commit ( token , b , expiry )
}
2021-11-26 13:00:03 +01:00
func ( s * SessionManager ) doStoreAll ( ctx context . Context ) ( map [ string ] [ ] byte , error ) {
cs , ok := s . Store . ( CtxStore )
if ok {
return cs . AllCtx ( ctx )
}
is , ok := s . Store . ( IterableStore )
if ok {
return is . All ( )
}
panic ( fmt . Sprintf ( "type %T does not support iteration" , s . Store ) )
}