2017-02-15 19:06:19 +01:00
package sessions
import (
"net/http"
"time"
2019-10-25 00:27:02 +02:00
"github.com/kataras/iris/v12/context"
2017-02-15 19:06:19 +01:00
)
2020-05-06 03:45:12 +02:00
func init ( ) {
context . SetHandlerName ( "iris/sessions.*Handler" , "iris.session" )
}
2020-05-17 21:08:43 +02:00
// A Sessions manager should be responsible to Start/Get a sesion, based
// on a Context, which returns a *Session, type.
// It performs automatic memory cleanup on expired sessions.
// It can accept a `Database` for persistence across server restarts.
// A session can set temporarly values (flash messages).
2017-07-10 17:32:42 +02:00
type Sessions struct {
config Config
provider * provider
2020-05-09 13:04:51 +02:00
handlerCookieOpts [ ] context . CookieOption // see `Handler`.
2017-07-10 17:32:42 +02:00
}
2017-02-15 19:06:19 +01:00
// New returns a new fast, feature-rich sessions manager
2017-07-10 17:32:42 +02:00
// it can be adapted to an iris station
func New ( cfg Config ) * Sessions {
return & Sessions {
2017-02-15 19:06:19 +01:00
config : cfg . Validate ( ) ,
provider : newProvider ( ) ,
}
}
// UseDatabase adds a session database to the manager's provider,
// a session db doesn't have write access
2017-07-10 17:32:42 +02:00
func ( s * Sessions ) UseDatabase ( db Database ) {
2017-02-15 19:06:19 +01:00
s . provider . RegisterDatabase ( db )
}
2020-05-09 13:04:51 +02:00
// GetCookieOptions returns any cookie options registered for the `Handler` method.
func ( s * Sessions ) GetCookieOptions ( ) [ ] context . CookieOption {
return s . handlerCookieOpts
}
2017-07-31 20:49:30 +02:00
// updateCookie gains the ability of updating the session browser cookie to any method which wants to update it
2019-02-16 20:03:48 +01:00
func ( s * Sessions ) updateCookie ( ctx context . Context , sid string , expires time . Duration , options ... context . CookieOption ) {
2017-07-31 20:49:30 +02:00
cookie := & http . Cookie { }
2017-02-15 19:06:19 +01:00
2017-07-31 20:49:30 +02:00
// The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding
cookie . Name = s . config . Cookie
cookie . Value = sid
cookie . Path = "/"
cookie . HttpOnly = true
2019-08-16 11:41:20 +02:00
2017-07-31 20:49:30 +02:00
// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
if expires >= 0 {
if expires == 0 { // unlimited life
2020-05-09 13:04:51 +02:00
cookie . Expires = context . CookieExpireUnlimited
2017-07-31 20:49:30 +02:00
} else { // > 0
cookie . Expires = time . Now ( ) . Add ( expires )
2017-02-15 19:06:19 +01:00
}
2020-02-02 15:29:06 +01:00
cookie . MaxAge = int ( time . Until ( cookie . Expires ) . Seconds ( ) )
2017-07-31 20:49:30 +02:00
}
2017-02-15 19:06:19 +01:00
2020-05-09 13:04:51 +02:00
ctx . UpsertCookie ( cookie , options ... )
2017-07-31 20:49:30 +02:00
}
2019-02-16 20:03:48 +01:00
// Start creates or retrieves an existing session for the particular request.
2020-05-09 13:04:51 +02:00
// Note that `Start` method will not respect configuration's `AllowReclaim`, `DisableSubdomainPersistence`, `CookieSecureTLS`,
// and `Encoding` settings.
// Register sessions as a middleware through the `Handler` method instead,
// which provides automatic resolution of a *sessions.Session input argument
// on MVC and APIContainer as well.
//
// NOTE: Use `app.Use(sess.Handler())` instead, avoid using `Start` manually.
2019-02-16 20:03:48 +01:00
func ( s * Sessions ) Start ( ctx context . Context , cookieOptions ... context . CookieOption ) * Session {
2020-05-09 13:04:51 +02:00
cookieValue := ctx . GetCookie ( s . config . Cookie , cookieOptions ... )
2017-07-31 20:49:30 +02:00
2019-07-24 18:51:42 +02:00
if cookieValue == "" { // cookie doesn't exist, let's generate a session and set a cookie.
2019-07-23 16:47:28 +02:00
sid := s . config . SessionIDGenerator ( ctx )
2017-07-31 20:49:30 +02:00
2020-04-13 21:15:55 +02:00
sess := s . provider . Init ( s , sid , s . config . Expires )
2018-04-22 12:52:36 +02:00
sess . isNew = s . provider . db . Len ( sid ) == 0
2017-08-01 10:00:30 +02:00
2019-02-16 20:03:48 +01:00
s . updateCookie ( ctx , sid , s . config . Expires , cookieOptions ... )
2017-03-18 11:22:20 +01:00
2017-07-10 17:32:42 +02:00
return sess
2017-02-15 19:06:19 +01:00
}
2017-07-10 17:32:42 +02:00
2020-04-13 21:15:55 +02:00
return s . provider . Read ( s , cookieValue , s . config . Expires )
2019-07-24 18:51:42 +02:00
}
2020-05-09 13:04:51 +02:00
const sessionContextKey = "iris.session"
2019-07-24 18:51:42 +02:00
// Handler returns a sessions middleware to register on application routes.
2020-05-09 13:04:51 +02:00
// To return the request's Session call the `Get(ctx)` package-level function.
//
// Call `Handler()` once per sessions manager.
2019-07-24 18:51:42 +02:00
func ( s * Sessions ) Handler ( cookieOptions ... context . CookieOption ) context . Handler {
2020-05-09 13:04:51 +02:00
s . handlerCookieOpts = cookieOptions
2020-05-10 01:17:28 +02:00
var requestOptions [ ] context . CookieOption
if s . config . AllowReclaim {
requestOptions = append ( requestOptions , context . CookieAllowReclaim ( s . config . Cookie ) )
}
if ! s . config . DisableSubdomainPersistence {
requestOptions = append ( requestOptions , context . CookieAllowSubdomains ( s . config . Cookie ) )
}
if s . config . CookieSecureTLS {
requestOptions = append ( requestOptions , context . CookieSecure )
}
if s . config . Encoding != nil {
requestOptions = append ( requestOptions , context . CookieEncoding ( s . config . Encoding , s . config . Cookie ) )
}
2019-07-24 18:51:42 +02:00
return func ( ctx context . Context ) {
2020-05-09 13:04:51 +02:00
ctx . AddCookieOptions ( requestOptions ... ) // request life-cycle options.
session := s . Start ( ctx , cookieOptions ... ) // this cookie's end-developer's custom options.
ctx . Values ( ) . Set ( sessionContextKey , session )
2019-07-24 18:51:42 +02:00
ctx . Next ( )
}
}
// Get returns a *Session from the same request life cycle,
// can be used inside a chain of handlers of a route.
//
// The `Sessions.Start` should be called previously,
// e.g. register the `Sessions.Handler` as middleware.
// Then call `Get` package-level function as many times as you want.
2020-05-09 13:04:51 +02:00
// Note: It will return nil if the session got destroyed by the same request.
// If you need to destroy and start a new session in the same request you need to call
// sessions manager's `Start` method after Destroy.
2019-07-24 18:51:42 +02:00
func Get ( ctx context . Context ) * Session {
2020-05-09 13:04:51 +02:00
if v := ctx . Values ( ) . Get ( sessionContextKey ) ; v != nil {
2019-07-24 18:51:42 +02:00
if sess , ok := v . ( * Session ) ; ok {
return sess
}
}
2017-07-10 17:32:42 +02:00
2020-05-09 13:04:51 +02:00
// ctx.Application().Logger().Debugf("Sessions: Get: no session found, prior Destroy(ctx) calls in the same request should follow with a Start(ctx) call too")
2019-07-24 18:51:42 +02:00
return nil
2017-02-15 19:06:19 +01:00
}
2019-02-16 20:03:48 +01:00
// StartWithPath same as `Start` but it explicitly accepts the cookie path option.
func ( s * Sessions ) StartWithPath ( ctx context . Context , path string ) * Session {
return s . Start ( ctx , context . CookiePath ( path ) )
}
2017-08-14 15:21:51 +02:00
// ShiftExpiration move the expire date of a session to a new date
2017-08-07 05:04:35 +02:00
// by using session default timeout configuration.
2018-08-14 15:29:04 +02:00
// It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet.
2019-02-16 20:03:48 +01:00
func ( s * Sessions ) ShiftExpiration ( ctx context . Context , cookieOptions ... context . CookieOption ) error {
return s . UpdateExpiration ( ctx , s . config . Expires , cookieOptions ... )
2017-07-31 20:49:30 +02:00
}
2017-08-14 15:21:51 +02:00
// UpdateExpiration change expire date of a session to a new date
2017-08-07 05:04:35 +02:00
// by using timeout value passed by `expires` receiver.
2018-08-14 15:29:04 +02:00
// It will return `ErrNotFound` when trying to update expiration on a non-existence or not valid session entry.
// It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet.
2019-02-16 20:03:48 +01:00
func ( s * Sessions ) UpdateExpiration ( ctx context . Context , expires time . Duration , cookieOptions ... context . CookieOption ) error {
2020-05-09 13:04:51 +02:00
cookieValue := ctx . GetCookie ( s . config . Cookie )
2018-08-14 15:29:04 +02:00
if cookieValue == "" {
return ErrNotFound
}
2017-08-01 07:34:18 +02:00
2018-08-14 15:29:04 +02:00
// we should also allow it to expire when the browser closed
err := s . provider . UpdateExpiration ( cookieValue , expires )
if err == nil || expires == - 1 {
2019-02-16 20:03:48 +01:00
s . updateCookie ( ctx , cookieValue , expires , cookieOptions ... )
2017-07-31 20:49:30 +02:00
}
2018-08-14 15:29:04 +02:00
return err
2017-07-31 20:49:30 +02:00
}
2018-04-22 13:13:40 +02:00
// DestroyListener is the form of a destroy listener.
// Look `OnDestroy` for more.
type DestroyListener func ( sid string )
// OnDestroy registers one or more destroy listeners.
// A destroy listener is fired when a session has been removed entirely from the server (the entry) and client-side (the cookie).
// Note that if a destroy listener is blocking, then the session manager will delay respectfully,
// use a goroutine inside the listener to avoid that behavior.
func ( s * Sessions ) OnDestroy ( listeners ... DestroyListener ) {
for _ , ln := range listeners {
s . provider . registerDestroyListener ( ln )
}
}
2020-05-09 13:04:51 +02:00
// Destroy removes the session data, the associated cookie
// and the Context's session value.
// Next calls of `sessions.Get` will occur to a nil Session,
// use `Sessions#Start` method for renewal
// or use the Session's Destroy method which does keep the session entry with its values cleared.
2017-07-10 17:32:42 +02:00
func ( s * Sessions ) Destroy ( ctx context . Context ) {
2020-05-09 13:04:51 +02:00
cookieValue := ctx . GetCookie ( s . config . Cookie )
2017-02-15 19:06:19 +01:00
if cookieValue == "" { // nothing to destroy
return
}
2017-03-18 11:22:20 +01:00
2020-05-09 13:04:51 +02:00
ctx . Values ( ) . Remove ( sessionContextKey )
ctx . RemoveCookie ( s . config . Cookie )
2017-02-15 19:06:19 +01:00
s . provider . Destroy ( cookieValue )
}
// DestroyByID removes the session entry
// from the server-side memory (and database if registered).
// Client's session cookie will still exist but it will be reseted on the next request.
//
// It's safe to use it even if you are not sure if a session with that id exists.
2017-03-18 22:43:04 +01:00
//
// Note: the sid should be the original one (i.e: fetched by a store )
// it's not decoded.
2017-07-10 17:32:42 +02:00
func ( s * Sessions ) DestroyByID ( sid string ) {
2017-02-15 19:06:19 +01:00
s . provider . Destroy ( sid )
}
// DestroyAll removes all sessions
// from the server-side memory (and database if registered).
// Client's session cookie will still exist but it will be reseted on the next request.
2017-07-10 17:32:42 +02:00
func ( s * Sessions ) DestroyAll ( ) {
2017-02-15 19:06:19 +01:00
s . provider . DestroyAll ( )
}