2016-05-30 16:08:09 +02:00
package sessions
import (
"encoding/base64"
2016-06-30 04:58:04 +02:00
"strings"
2016-05-30 16:08:09 +02:00
"sync"
"time"
"github.com/kataras/iris/config"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions/store"
"github.com/kataras/iris/utils"
"github.com/valyala/fasthttp"
)
type (
// IManager is the interface which Manager should implement
IManager interface {
Start ( context . IContext ) store . IStore
Destroy ( context . IContext )
GC ( )
}
// Manager implements the IManager interface
// contains the cookie's name, the provider and a duration for GC and cookie life expire
Manager struct {
2016-06-30 13:26:42 +02:00
config * config . Sessions
provider IProvider
mu sync . Mutex
2016-05-30 16:08:09 +02:00
}
)
var _ IManager = & Manager { }
var (
continueOnError = true
providers = make ( map [ string ] IProvider )
)
// newManager creates & returns a new Manager
func newManager ( c config . Sessions ) ( * Manager , error ) {
provider , found := providers [ c . Provider ]
if ! found {
return nil , ErrProviderNotFound . Format ( c . Provider )
}
2016-07-01 00:38:29 +02:00
if c . DecodeCookie {
c . Cookie = base64 . URLEncoding . EncodeToString ( [ ] byte ( c . Cookie ) ) // change the cookie's name/key to a more safe(?)
// get the real value for your tests by:
//sessIdKey := url.QueryEscape(base64.URLEncoding.EncodeToString([]byte(iris.Config.Sessions.Cookie)))
}
2016-05-30 16:08:09 +02:00
manager := & Manager { }
manager . config = & c
manager . provider = provider
return manager , nil
}
// Register registers a provider
func Register ( provider IProvider ) {
if provider == nil {
ErrProviderRegister . Panic ( )
}
providerName := provider . Name ( )
if _ , exists := providers [ providerName ] ; exists {
if ! continueOnError {
ErrProviderAlreadyExists . Panicf ( providerName )
} else {
// do nothing it's a map it will overrides the existing provider.
}
}
providers [ providerName ] = provider
}
// Manager implementation
func ( m * Manager ) generateSessionID ( ) string {
return base64 . URLEncoding . EncodeToString ( utils . Random ( 32 ) )
}
2016-06-30 04:58:04 +02:00
var dotB = byte ( '.' )
2016-05-30 16:08:09 +02:00
// Start starts the session
func ( m * Manager ) Start ( ctx context . IContext ) store . IStore {
m . mu . Lock ( )
var store store . IStore
requestCtx := ctx . GetRequestCtx ( )
2016-06-30 13:26:42 +02:00
cookieValue := string ( requestCtx . Request . Header . Cookie ( m . config . Cookie ) )
2016-05-30 16:08:09 +02:00
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
sid := m . generateSessionID ( )
store , _ = m . provider . Init ( sid )
cookie := fasthttp . AcquireCookie ( )
2016-06-30 04:58:04 +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
2016-06-30 13:26:42 +02:00
cookie . SetKey ( m . config . Cookie )
2016-07-01 00:38:29 +02:00
cookie . SetValue ( sid )
2016-05-30 16:08:09 +02:00
cookie . SetPath ( "/" )
2016-06-30 16:55:33 +02:00
if ! m . config . DisableSubdomainPersistence {
2016-06-30 05:02:07 +02:00
requestDomain := ctx . HostString ( )
2016-06-30 16:55:33 +02:00
if portIdx := strings . IndexByte ( requestDomain , ':' ) ; portIdx > 0 {
requestDomain = requestDomain [ 0 : portIdx ]
}
2016-07-03 16:21:57 +02:00
2016-06-30 16:55:33 +02:00
if requestDomain == "0.0.0.0" || requestDomain == "127.0.0.1" {
// for these type of hosts, we can't allow subdomains persistance,
// the web browser doesn't understand the mysubdomain.0.0.0.0 and mysubdomain.127.0.0.1 as scorrectly ubdomains because of the many dots
2016-07-03 16:21:57 +02:00
// so don't set a domain here
2016-06-30 16:55:33 +02:00
} else if strings . Count ( requestDomain , "." ) > 0 { // there is a problem with .localhost setted as the domain, so we check that first
2016-06-30 13:26:42 +02:00
// RFC2109, we allow level 1 subdomains, but no further
// if we have localhost.com , we want the localhost.com.
// so if we have something like: mysubdomain.localhost.com we want the localhost here
// if we have mysubsubdomain.mysubdomain.localhost.com we want the .mysubdomain.localhost.com here
// slow things here, especially the 'replace' but this is a good and understable( I hope) way to get the be able to set cookies from subdomains & domain with 1-level limit
if dotIdx := strings . LastIndexByte ( requestDomain , dotB ) ; dotIdx > 0 {
// is mysubdomain.localhost.com || mysubsubdomain.mysubdomain.localhost.com
s := requestDomain [ 0 : dotIdx ] // set mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost
if secondDotIdx := strings . LastIndexByte ( s , dotB ) ; secondDotIdx > 0 {
//is mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost
s = s [ secondDotIdx + 1 : ] // set to localhost || mysubdomain.localhost
}
// replace the s with the requestDomain before the domain's siffux
subdomainSuff := strings . LastIndexByte ( requestDomain , dotB )
if subdomainSuff > len ( s ) { // if it is actual exists as subdomain suffix
requestDomain = strings . Replace ( requestDomain , requestDomain [ 0 : subdomainSuff ] , s , 1 ) // set to localhost.com || mysubdomain.localhost.com
}
2016-06-30 04:58:04 +02:00
}
2016-06-30 13:26:42 +02:00
// finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow)
cookie . SetDomain ( "." + requestDomain ) // . to allow persistance
2016-06-30 04:58:04 +02:00
}
2016-06-30 13:26:42 +02:00
2016-06-30 04:58:04 +02:00
}
2016-05-30 16:08:09 +02:00
cookie . SetHTTPOnly ( true )
cookie . SetExpire ( m . config . Expires )
requestCtx . Response . Header . SetCookie ( cookie )
fasthttp . ReleaseCookie ( cookie )
} else {
2016-07-01 00:38:29 +02:00
store , _ = m . provider . Read ( cookieValue )
2016-05-30 16:08:09 +02:00
}
m . mu . Unlock ( )
return store
}
// Destroy kills the session and remove the associated cookie
func ( m * Manager ) Destroy ( ctx context . IContext ) {
cookieValue := string ( ctx . GetRequestCtx ( ) . Request . Header . Cookie ( m . config . Cookie ) )
if cookieValue == "" { // nothing to destroy
return
}
m . mu . Lock ( )
m . provider . Destroy ( cookieValue )
ctx . RemoveCookie ( m . config . Cookie )
m . mu . Unlock ( )
}
// GC tick-tock for the store cleanup
// it's a blocking function, so run it with go routine, it's totally safe
func ( m * Manager ) GC ( ) {
m . mu . Lock ( )
m . provider . GC ( m . config . GcDuration )
// set a timer for the next GC
time . AfterFunc ( m . config . GcDuration , func ( ) {
m . GC ( )
} ) // or m.expire.Unix() if Nanosecond() doesn't works here
m . mu . Unlock ( )
}