🌈 sessions were re-written, update to 4.0.0-alpha.2, read HISTORY.md

**Sessions were re-written **

- Developers can use more than one 'session database', at the same time,
to store the sessions
- Easy to develop a custom session database (only two functions are
required (Load & Update)), [learn
more](https://github.com/iris-contrib/sessiondb/blob/master/redis/database.go)
- Session databases are located
[here](https://github.com/iris-contrib/sessiondb), contributions are
welcome
- The only frontend deleted 'thing' is the: **config.Sessions.Provider**
- No need to register a database, the sessions works out-of-the-box
- No frontend/API changes except the
`context.Session().Set/Delete/Clear`, they doesn't return errors
anymore, btw they (errors) were always nil :)
- Examples (master branch) were updated.

```sh
$ go get github.com/iris-contrib/sessiondb/$DATABASE
```

```go
db := $DATABASE.New(configurationHere{})
iris.UseSessionDB(db)
```

> Note: Book is not updated yet, examples are up-to-date as always.
This commit is contained in:
Makis Maropoulos 2016-07-15 20:50:36 +03:00
parent af4df18ec4
commit 077984bd60
20 changed files with 733 additions and 1582 deletions

View File

@ -2,6 +2,30 @@
**How to upgrade**: remove your `$GOPATH/src/github.com/kataras/iris` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`.
## 4.0.0-alpha.1 -> 4.0.0-alpha.2
**Sessions were re-written **
- Developers can use more than one 'session database', at the same time, to store the sessions
- Easy to develop a custom session database (only two functions are required (Load & Update)), [learn more](https://github.com/iris-contrib/sessiondb/blob/master/redis/database.go)
- Session databases are located [here](https://github.com/iris-contrib/sessiondb), contributions are welcome
- The only frontend deleted 'thing' is the: **config.Sessions.Provider**
- No need to register a database, the sessions works out-of-the-box
- No frontend/API changes except the `context.Session().Set/Delete/Clear`, they doesn't return errors anymore, btw they (errors) were always nil :)
- Examples (master branch) were updated.
```sh
$ go get github.com/iris-contrib/sessiondb/$DATABASE
```
```go
db := $DATABASE.New(configurationHere{})
iris.UseSessionDB(db)
```
> Note: Book is not updated yet, examples are up-to-date as always.
## 3.0.0 -> 4.0.0-alpha.1

View File

@ -145,7 +145,7 @@ I recommend writing your API tests using this new library, [httpexpect](https://
Versioning
------------
Current: **v4.0.0-alpha.1**
Current: **v4.0.0-alpha.2**
> Iris is an active project
@ -157,7 +157,7 @@ Todo
> for 'v4'
- [x] Refactor & extend view engine, separate the engines from the main code base, easier for the community to create new view engines
- [ ] Refactor & extend sessions, split the providers and stores to the iris-contrib
- [x] Refactor & extend sessions, split the different databases functionality to the iris-contrib
- [ ] Refactor & extends the rest render engine in order to be able to developer to use their own implemention for rendering restful types, like, for example a custom JSON implementation using no-standard go package for encode/decode
- [ ] Move the iris/websocket package's source code inside iris/websocket.go one file, to be easier to use by users without import a new package
- [ ] configs package should be removed after all these, we will not need big configurations because of different packages splitted & moved to the iris-contrib, we will keep interfaces and all required things inside kataras/iris.go.
@ -192,7 +192,7 @@ License can be found [here](LICENSE).
[Travis]: http://travis-ci.org/kataras/iris
[License Widget]: https://img.shields.io/badge/license-MIT%20%20License%20-E91E63.svg?style=flat-square
[License]: https://github.com/kataras/iris/blob/master/LICENSE
[Release Widget]: https://img.shields.io/badge/release-v4.0.0--alpha.1-blue.svg?style=flat-square
[Release Widget]: https://img.shields.io/badge/release-v4.0.0--alpha.2-blue.svg?style=flat-square
[Release]: https://github.com/kataras/iris/releases
[Chat Widget]: https://img.shields.io/badge/community-chat-00BCD4.svg?style=flat-square
[Chat]: https://kataras.rocket.chat/channel/iris

View File

@ -17,50 +17,18 @@ const (
DefaultCookieName = "irissessionid"
// DefaultSessionGcDuration is the default Session Manager's GCDuration , which is 2 hours
DefaultSessionGcDuration = time.Duration(2) * time.Hour
// DefaultRedisNetwork the redis network option, "tcp"
DefaultRedisNetwork = "tcp"
// DefaultRedisAddr the redis address option, "127.0.0.1:6379"
DefaultRedisAddr = "127.0.0.1:6379"
// DefaultRedisIdleTimeout the redis idle timeout option, time.Duration(5) * time.Minute
DefaultRedisIdleTimeout = time.Duration(5) * time.Minute
// DefaultRedisMaxAgeSeconds the redis storage last parameter (SETEX), 31556926.0 (1 year)
DefaultRedisMaxAgeSeconds = 31556926.0 //1 year
)
type (
// Redis the redis configuration used inside sessions
Redis struct {
// Network "tcp"
Network string
// Addr "127.0.0.1:6379"
Addr string
// Password string .If no password then no 'AUTH'. Default ""
Password string
// If Database is empty "" then no 'SELECT'. Default ""
Database string
// MaxIdle 0 no limit
MaxIdle int
// MaxActive 0 no limit
MaxActive int
// IdleTimeout time.Duration(5) * time.Minute
IdleTimeout time.Duration
// Prefix "myprefix-for-this-website". Default ""
Prefix string
// MaxAgeSeconds how much long the redis should keep the session in seconds. Default 31556926.0 (1 year)
MaxAgeSeconds int
}
// Sessions the configuration for sessions
// has 4 fields
// first is the providerName (string) ["memory","redis"]
// second is the cookieName, the session's name (string) ["mysessionsecretcookieid"]
// has 5 fields
// first is the cookieName, the session's name (string) ["mysessionsecretcookieid"]
// second enable if you want to decode the cookie's key also
// third is the time which the client's cookie expires
// forth is the gcDuration (time.Duration) when this time passes it removes the unused sessions from the memory until the user come back
// fifth is the DisableSubdomainPersistence which you can set it to true in order dissallow your iris subdomains to have access to the session cook
Sessions struct {
// Provider string, usage iris.Config().Provider = "memory" or "redis". If you wan to customize redis then import the package, and change it's config
Provider string
// Cookie string, the session's client cookie name, for example: "irissessionid"
Cookie string
// DecodeCookie set it to true to decode the cookie key with base64 URLEncoding
@ -75,7 +43,7 @@ type (
// Default 2 hours
GcDuration time.Duration
// DisableSubdomainPersistence set it to dissallow your iris subdomains to have access to the session cookie
// DisableSubdomainPersistence set it to true in order dissallow your iris subdomains to have access to the session cookie
// defaults to false
DisableSubdomainPersistence bool
}
@ -84,7 +52,6 @@ type (
// DefaultSessions the default configs for Sessions
func DefaultSessions() Sessions {
return Sessions{
Provider: "memory", // the default provider is "memory", if you set it to "" means that sessions are disabled.
Cookie: DefaultCookieName,
DecodeCookie: false,
Expires: CookieExpireNever,
@ -115,41 +82,3 @@ func (c Sessions) MergeSingle(cfg Sessions) (config Sessions) {
return
}
// DefaultRedis returns the default configuration for Redis service
func DefaultRedis() Redis {
return Redis{
Network: DefaultRedisNetwork,
Addr: DefaultRedisAddr,
Password: "",
Database: "",
MaxIdle: 0,
MaxActive: 0,
IdleTimeout: DefaultRedisIdleTimeout,
Prefix: "",
MaxAgeSeconds: DefaultRedisMaxAgeSeconds,
}
}
// Merge merges the default with the given config and returns the result
func (c Redis) Merge(cfg []Redis) (config Redis) {
if len(cfg) > 0 {
config = cfg[0]
mergo.Merge(&config, c)
} else {
_default := c
config = _default
}
return
}
// MergeSingle merges the default with the given config and returns the result
func (c Redis) MergeSingle(cfg Redis) (config Redis) {
config = cfg
mergo.Merge(&config, c)
return
}

View File

@ -25,7 +25,6 @@ import (
"github.com/iris-contrib/formBinder"
"github.com/kataras/iris/config"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions/store"
"github.com/kataras/iris/utils"
"github.com/klauspost/compress/gzip"
"github.com/valyala/fasthttp"
@ -76,10 +75,6 @@ var (
)
type (
// RenderOptions is a helper type for the optional runtime options can be passed by user when Render
// an example of this is the "layout" or "gzip" option
// same as Map but more specific name
RenderOptions map[string]interface{}
// Map is just a conversion for a map[string]interface{}
// should not be used inside Render when PongoEngine is used.
@ -91,8 +86,8 @@ type (
Params PathParameters
framework *Framework
//keep track all registed middleware (handlers)
middleware Middleware
sessionStore store.IStore
middleware Middleware
session *session
// pos is the position number of the Context, look .Next to understand
pos uint8
}
@ -110,7 +105,7 @@ func (ctx *Context) GetRequestCtx() *fasthttp.RequestCtx {
// I use it for zero rellocation memory
func (ctx *Context) Reset(reqCtx *fasthttp.RequestCtx) {
ctx.Params = ctx.Params[0:0]
ctx.sessionStore = nil
ctx.session = nil
ctx.middleware = nil
ctx.RequestCtx = reqCtx
}
@ -853,24 +848,32 @@ func (ctx *Context) SetFlash(key string, value string) {
fasthttp.ReleaseCookie(c)
}
// Session returns the current session store, returns nil if provider is ""
func (ctx *Context) Session() store.IStore {
if ctx.framework.sessions == nil || ctx.framework.Config.Sessions.Provider == "" { //the second check can be changed on runtime, users are able to turn off the sessions by setting provider to ""
// Session returns the current session
func (ctx *Context) Session() interface {
ID() string
Get(string) interface{}
GetString(key string) string
GetInt(key string) int
GetAll() map[string]interface{}
VisitAll(cb func(k string, v interface{}))
Set(string, interface{})
Delete(string)
Clear()
} {
if ctx.framework.sessions == nil { // this should never return nil but FOR ANY CASE, on future changes.
return nil
}
if ctx.sessionStore == nil {
ctx.sessionStore = ctx.framework.sessions.Start(ctx)
if ctx.session == nil {
ctx.session = ctx.framework.sessions.start(ctx)
}
return ctx.sessionStore
return ctx.session
}
// SessionDestroy destroys the whole session, calls the provider's destroy and remove the cookie
func (ctx *Context) SessionDestroy() {
if ctx.framework.sessions != nil {
if store := ctx.Session(); store != nil {
ctx.framework.sessions.Destroy(ctx)
}
if sess := ctx.Session(); sess != nil {
ctx.framework.sessions.destroy(ctx)
}
}

View File

@ -5,11 +5,11 @@ import (
"io"
"time"
"github.com/kataras/iris/sessions/store"
"github.com/valyala/fasthttp"
)
type (
// IContext the interface for the iris/context
// Used mostly inside packages which shouldn't be import ,directly, the kataras/iris.
IContext interface {
@ -71,7 +71,17 @@ type (
GetFlashes() map[string]string
GetFlash(string) (string, error)
SetFlash(string, string)
Session() store.IStore
Session() interface {
ID() string
Get(string) interface{}
GetString(key string) string
GetInt(key string) int
GetAll() map[string]interface{}
VisitAll(cb func(k string, v interface{}))
Set(string, interface{})
Delete(string)
Clear()
}
SessionDestroy()
Log(string, ...interface{})
Reset(*fasthttp.RequestCtx)

View File

@ -596,7 +596,7 @@ func TestContextSessions(t *testing.T) {
// test destory which also clears first
d := e.GET("/destroy").Expect().Status(StatusOK)
d.JSON().Object().Empty()
d.JSON().Null()
// This removed: d.Cookies().Empty(). Reason:
// httpexpect counts the cookies setted or deleted at the response time, but cookie is not removed, to be really removed needs to SetExpire(now-1second) so,
// test if the cookies removed on the next request, like the browser's behavior.

63
iris.go
View File

@ -67,22 +67,17 @@ import (
"github.com/iris-contrib/errors"
"github.com/iris-contrib/logger"
"github.com/iris-contrib/rest"
"github.com/iris-contrib/template"
"github.com/iris-contrib/template/html"
"github.com/kataras/iris/config"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/utils"
"github.com/kataras/iris/websocket"
"github.com/valyala/fasthttp"
///NOTE: register the session providers, but the s.Config.Sessions.Provider will be used only, if this empty then sessions are disabled.
_ "github.com/kataras/iris/sessions/providers/memory"
_ "github.com/kataras/iris/sessions/providers/redis"
)
const (
// Version of the iris
Version = "4.0.0-alpha.1"
Version = "4.0.0-alpha.2"
banner = ` _____ _
|_ _| (_)
@ -90,9 +85,6 @@ const (
| | | __|| |/ __|
_| |_| | | |\__ \
|_____|_| |_||___/ ` + Version + ` `
// NoLayout pass it to the layout option on the context.Render to disable layout for this execution
NoLayout = template.NoLayout
)
// Default entry, use it with iris.$anyPublicFunc
@ -148,7 +140,8 @@ type (
ListenVirtual(...string) *Server
Go() error
Close() error
UseTemplate(template.TemplateEngine) *template.TemplateEngineLocation
UseSessionDB(SessionDatabase)
UseTemplate(TemplateEngine) *TemplateEngineLocation
UseGlobal(...Handler)
UseGlobalFunc(...HandlerFunc)
OnError(int, HandlerFunc)
@ -167,8 +160,8 @@ type (
Framework struct {
*muxAPI
rest *rest.Render
sessions *sessions.Manager
templates *template.TemplateEngines
sessions *sessionsManager
templates *TemplateEngines
// fields which are useful to the user/dev
// the last added server is the main server
@ -203,13 +196,15 @@ func New(cfg ...config.Iris) *Framework {
// set the plugin container
s.Plugins = &pluginContainer{logger: s.Logger}
// set the templates
s.templates = &template.TemplateEngines{
s.templates = &TemplateEngines{
Helpers: map[string]interface{}{
"url": s.URL,
"urlpath": s.Path,
},
Engines: make([]*template.TemplateEngineWrapper, 0),
Engines: make([]*TemplateEngineWrapper, 0),
}
//set the session manager
s.sessions = newSessionsManager(c.Sessions)
// set the websocket server
s.Websocket = websocket.NewServer(s.Config.Websocket)
// set the servemux, which will provide us the public API also, with its context pool
@ -225,11 +220,6 @@ func New(cfg ...config.Iris) *Framework {
}
func (s *Framework) initialize() {
// set sessions
if s.Config.Sessions.Provider != "" {
s.sessions = sessions.New(s.Config.Sessions)
}
// set the rest
s.rest = rest.New(s.Config.Rest)
// prepare the templates if enabled
@ -476,28 +466,35 @@ func (s *Framework) Close() error {
return s.Servers.CloseAll()
}
/*
// UseSessionDB registers a session database, you can register more than one
// accepts a session database which implements a Load(sid string) map[string]interface{} and an Update(sid string, newValues map[string]interface{})
// the only reason that a session database will be useful for you is when you want to keep the session's values/data after the app restart
// a session database doesn't have write access to the session, it doesn't accept the context, so forget 'cookie database' for sessions, I will never allow that, for your protection.
//
// Note: Don't worry if no session database is registered, your context.Session will continue to work.
func UseSessionDB(db SessionDatabase) {
Default.UseSessionDB(db)
}
// set the template engines
s.renderer = &renderer{
engines: make([]TemplateEngine, 0),
buffer: utils.NewBufferPool(64),
helpers: map[string]interface{}{
"url": s.URL,
"urlpath": s.Path,
},
contentType: s.Config.Render.Template.ContentType + "; " + s.Config.Render.Template.Charset,
}*/
// UseSessionDB registers a session database, you can register more than one
// accepts a session database which implements a Load(sid string) map[string]interface{} and an Update(sid string, newValues map[string]interface{})
// the only reason that a session database will be useful for you is when you want to keep the session's values/data after the app restart
// a session database doesn't have write access to the session, it doesn't accept the context, so forget 'cookie database' for sessions, I will never allow that, for your protection.
//
// Note: Don't worry if no session database is registered, your context.Session will continue to work.
func (s *Framework) UseSessionDB(db SessionDatabase) {
s.sessions.provider.registerDatabase(db)
}
// UseTemplate adds a template engine to the iris view system
// it does not build/load them yet
func UseTemplate(e template.TemplateEngine) *template.TemplateEngineLocation {
func UseTemplate(e TemplateEngine) *TemplateEngineLocation {
return Default.UseTemplate(e)
}
// UseTemplate adds a template engine to the iris view system
// it does not build/load them yet
func (s *Framework) UseTemplate(e template.TemplateEngine) *template.TemplateEngineLocation {
func (s *Framework) UseTemplate(e TemplateEngine) *TemplateEngineLocation {
return s.templates.Add(e)
}
@ -1561,7 +1558,7 @@ func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc
//
func (api *muxAPI) Layout(tmplLayoutFile string) MuxAPI {
api.UseFunc(func(ctx *Context) {
ctx.Set(template.TemplateLayoutContextKey, tmplLayoutFile)
ctx.Set(TemplateLayoutContextKey, tmplLayoutFile)
ctx.Next()
})
return api

355
sessions.go Normal file
View File

@ -0,0 +1,355 @@
package iris
import (
"container/list"
"encoding/base64"
"strings"
"sync"
"time"
"github.com/kataras/iris/config"
"github.com/kataras/iris/utils"
"github.com/valyala/fasthttp"
)
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------SessionDatabase implementation---------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// SessionDatabase is the interface which all session databases should implement
// By design it doesn't support any type of cookie store like other frameworks, I want to protect you, believe me, no context access (although we could)
// The scope of the database is to store somewhere the sessions in order to keep them after restarting the server, nothing more.
// the values are stored by the underline session, the check for new sessions, or 'this session value should added' are made automatically by Iris, you are able just to set the values to your backend database with Load function.
// session database doesn't have any write or read access to the session, the loading of the initial data is done by the Load(string) map[string]interfface{} function
// synchronization are made automatically, you can register more than one session database but the first non-empty Load return data will be used as the session values.
type SessionDatabase interface {
Load(string) map[string]interface{}
Update(string, map[string]interface{})
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------Session implementation-----------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// session is an 'object' which wraps the session provider with its session databases, only frontend user has access to this session object.
// this is really used on context and everywhere inside Iris
type session struct {
sid string
values map[string]interface{} // here is the real values
mu sync.Mutex
lastAccessedTime time.Time
provider *sessionProvider
}
// ID returns the session's id
func (s *session) ID() string {
return s.sid
}
// Get returns the value of an entry by its key
func (s *session) Get(key string) interface{} {
s.provider.update(s.sid)
if value, found := s.values[key]; found {
return value
}
return nil
}
// GetString same as Get but returns as string, if nil then returns an empty string
func (s *session) GetString(key string) string {
if value := s.Get(key); value != nil {
if v, ok := value.(string); ok {
return v
}
}
return ""
}
// GetInt same as Get but returns as int, if nil then returns -1
func (s *session) GetInt(key string) int {
if value := s.Get(key); value != nil {
if v, ok := value.(int); ok {
return v
}
}
return -1
}
// GetAll returns all session's values
func (s *session) GetAll() map[string]interface{} {
return s.values
}
// VisitAll loop each one entry and calls the callback function func(key,value)
func (s *session) VisitAll(cb func(k string, v interface{})) {
for key := range s.values {
cb(key, s.values[key])
}
}
// Set fills the session with an entry, it receives a key and a value
// returns an error, which is always nil
func (s *session) Set(key string, value interface{}) {
s.mu.Lock()
s.values[key] = value
s.mu.Unlock()
s.provider.update(s.sid)
}
// Delete removes an entry by its key
// returns an error, which is always nil
func (s *session) Delete(key string) {
s.mu.Lock()
delete(s.values, key)
s.mu.Unlock()
s.provider.update(s.sid)
}
// Clear removes all entries
func (s *session) Clear() {
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
s.provider.update(s.sid)
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------sessionProvider implementation---------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
type (
// sessionProvider contains the temp sessions memory and the databases
sessionProvider struct {
mu sync.Mutex
sessions map[string]*list.Element // underline TEMPORARY memory store used to give advantage on sessions used more times than others
list *list.List // for GC
databases []SessionDatabase
}
)
func (p *sessionProvider) registerDatabase(db SessionDatabase) {
p.mu.Lock() // for any case
p.databases = append(p.databases, db)
p.mu.Unlock()
}
func (p *sessionProvider) newSession(sid string) *session {
return &session{
sid: sid,
provider: p,
lastAccessedTime: time.Now(),
values: p.loadSessionValues(sid),
}
}
func (p *sessionProvider) loadSessionValues(sid string) map[string]interface{} {
for i, n := 0, len(p.databases); i < n; i++ {
if dbValues := p.databases[i].Load(sid); dbValues != nil && len(dbValues) > 0 {
return dbValues // return the first non-empty from the registered stores.
}
}
values := make(map[string]interface{})
return values
}
func (p *sessionProvider) updateDatabases(sid string, newValues map[string]interface{}) {
for i, n := 0, len(p.databases); i < n; i++ {
p.databases[i].Update(sid, newValues)
}
}
// Init creates the session and returns it
func (p *sessionProvider) init(sid string) *session {
newSession := p.newSession(sid)
elem := p.list.PushBack(newSession)
p.mu.Lock()
p.sessions[sid] = elem
p.mu.Unlock()
return newSession
}
// Read returns the store which sid parameter is belongs
func (p *sessionProvider) read(sid string) *session {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
p.mu.Unlock() // yes defer is slow
return elem.Value.(*session)
}
p.mu.Unlock()
// if not found create new
sess := p.init(sid)
return sess
}
// Destroy destroys the session, removes all sessions values, the session itself and updates the registered session databases, this called from sessionManager which removes the client's cookie also.
func (p *sessionProvider) destroy(sid string) {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
sess := elem.Value.(*session)
sess.values = nil
p.updateDatabases(sid, nil)
delete(p.sessions, sid)
p.list.Remove(elem)
}
p.mu.Unlock()
}
// Update updates the lastAccessedTime, and moves the memory place element to the front
// always returns a nil error, for now
func (p *sessionProvider) update(sid string) {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
sess := elem.Value.(*session)
sess.lastAccessedTime = time.Now()
p.list.MoveToFront(elem)
p.updateDatabases(sid, sess.values)
}
p.mu.Unlock()
}
// GC clears the memory
func (p *sessionProvider) gc(duration time.Duration) {
p.mu.Lock()
defer p.mu.Unlock()
for {
elem := p.list.Back()
if elem == nil {
break
}
// if the time has passed. session was expired, then delete the session and its memory place
// we are not destroy the session completely for the case this is re-used after
if (elem.Value.(*session).lastAccessedTime.Unix() + duration.Nanoseconds()) < time.Now().Unix() {
p.list.Remove(elem)
delete(p.sessions, elem.Value.(*session).sid)
} else {
break
}
}
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------sessionsManager implementation---------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
type (
// sessionsManager implements the ISessionsManager interface
// contains the cookie's name, the provider and a duration for GC and cookie life expire
sessionsManager struct {
config config.Sessions
provider *sessionProvider
}
)
// newSessionsManager creates & returns a new SessionsManager and start its GC
func newSessionsManager(c config.Sessions) *sessionsManager {
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)))
}
manager := &sessionsManager{config: c, provider: &sessionProvider{list: list.New(), sessions: make(map[string]*list.Element, 0), databases: make([]SessionDatabase, 0)}}
//run the GC here
go manager.gc()
return manager
}
func (m *sessionsManager) generateSessionID() string {
return base64.URLEncoding.EncodeToString(utils.Random(32))
}
// Start starts the session
func (m *sessionsManager) start(ctx *Context) *session {
var session *session
cookieValue := ctx.GetCookie(m.config.Cookie)
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
sid := m.generateSessionID()
session = m.provider.init(sid)
cookie := fasthttp.AcquireCookie()
// 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.SetKey(m.config.Cookie)
cookie.SetValue(sid)
cookie.SetPath("/")
if !m.config.DisableSubdomainPersistence {
requestDomain := ctx.HostString()
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
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
// so don't set a domain here
} else if strings.Count(requestDomain, ".") > 0 { // there is a problem with .localhost setted as the domain, so we check that first
// 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, '.'); 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, '.'); 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, '.')
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
}
}
// finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow)
cookie.SetDomain("." + requestDomain) // . to allow persistance
}
}
cookie.SetHTTPOnly(true)
cookie.SetExpire(m.config.Expires)
ctx.SetCookie(cookie)
fasthttp.ReleaseCookie(cookie)
} else {
session = m.provider.read(cookieValue)
}
return session
}
// Destroy kills the session and remove the associated cookie
func (m *sessionsManager) destroy(ctx *Context) {
cookieValue := ctx.GetCookie(m.config.Cookie)
if cookieValue == "" { // nothing to destroy
return
}
ctx.RemoveCookie(m.config.Cookie)
m.provider.destroy(cookieValue)
}
// GC tick-tock for the store cleanup
// it's a blocking function, so run it with go routine, it's totally safe
func (m *sessionsManager) gc() {
m.provider.gc(m.config.GcDuration)
// set a timer for the next GC
time.AfterFunc(m.config.GcDuration, func() {
m.gc()
})
}

View File

@ -1,442 +0,0 @@
# Folder Information
This folder contains the sessions support for Iris. The folder name is plural (session's') so the `/sessions/providers`, because you can use both of them at the same time.
# Package information
This package is new and unique, if you notice a bug or issue [post it here](https://github.com/kataras/iris/issues).
- Cleans the temp memory when a sessions is iddle, and re-loccate it , fast, to the temp memory when it's necessary. Also most used/regular sessions are going front in the memory's list.
- Supports redisstore and normal memory routing. If redisstore is used but fails to connect then ,automatically, switching to the memory storage.
**A session can be defined as a server-side storage of information that is desired to persist throughout the user's interaction with the web site** or web application.
Instead of storing large and constantly changing information via cookies in the user's browser, **only a unique identifier is stored on the client side** (called a "session id"). This session id is passed to the web server every time the browser makes an HTTP request (ie a page link or AJAX request). The web application pairs this session id with it's internal database/memory and retrieves the stored variables for use by the requested page.
----
You will see two different ways to use the sessions, I'm using the first. No performance differences.
## How to use - easy way
Example **memory**
```go
package main
import (
"github.com/kataras/iris"
)
func main() {
// these are the defaults
//iris.Config().Session.Provider = "memory"
//iris.Config().Session.Secret = "irissessionid"
//iris.Config().Session.Life = time.Duration(60) *time.Minute
iris.Get("/set", func(c *iris.Context) {
//set session values
c.Session().Set("name", "iris")
//test if setted here
c.Write("All ok session setted to: %s", c.Session().GetString("name"))
})
iris.Get("/get", func(c *iris.Context) {
name := c.Session().GetString("name")
c.Write("The name on the /set was: %s", name)
})
iris.Get("/delete", func(c *iris.Context) {
//get the session for this context
c.Session().Delete("name")
})
iris.Get("/clear", func(c *iris.Context) {
// removes all entries
c.Session().Clear()
})
iris.Get("/destroy", func(c *iris.Context) {
//destroy, removes the entire session and cookie
c.SessionDestroy()
})
println("Server is listening at :8080")
iris.Listen("8080")
}
```
Example default **redis**
```go
package main
import (
"github.com/kataras/iris"
)
func main() {
iris.Config().Session.Provider = "redis"
iris.Get("/set", func(c *iris.Context) {
//set session values
c.Session().Set("name", "iris")
//test if setted here
c.Write("All ok session setted to: %s", c.Session().GetString("name"))
})
iris.Get("/get", func(c *iris.Context) {
name := c.Session().GetString("name")
c.Write("The name on the /set was: %s", name)
})
iris.Get("/delete", func(c *iris.Context) {
//get the session for this context
c.Session().Delete("name")
})
iris.Get("/clear", func(c *iris.Context) {
// removes all entries
c.Session().Clear()
})
iris.Get("/destroy", func(c *iris.Context) {
//destroy, removes the entire session and cookie
c.SessionDestroy()
})
println("Server is listening at :8080")
iris.Listen("8080")
}
```
Example customized **redis**
```go
// Config the redis config
type Config struct {
// Network "tcp"
Network string
// Addr "127.0.01:6379"
Addr string
// Password string .If no password then no 'AUTH'. Default ""
Password string
// If Database is empty "" then no 'SELECT'. Default ""
Database string
// MaxIdle 0 no limit
MaxIdle int
// MaxActive 0 no limit
MaxActive int
// IdleTimeout 5 * time.Minute
IdleTimeout time.Duration
// Prefix "myprefix-for-this-website". Default ""
Prefix string
// MaxAgeSeconds how much long the redis should keep the session in seconds. Default 2520.0 (42minutes)
MaxAgeSeconds int
}
```
```go
package main
import (
"github.com/kataras/iris"
"github.com/kataras/iris/sessions/providers/redis"
)
func init() {
redis.Config.Addr = "127.0.0.1:2222"
redis.Config.MaxAgeSeconds = 5000.0
}
func main() {
iris.Config().Session.Provider = "redis"
iris.Get("/set", func(c *iris.Context) {
//set session values
c.Session().Set("name", "iris")
//test if setted here
c.Write("All ok session setted to: %s", c.Session().GetString("name"))
})
iris.Get("/get", func(c *iris.Context) {
name := c.Session().GetString("name")
c.Write("The name on the /set was: %s", name)
})
iris.Get("/delete", func(c *iris.Context) {
//get the session for this context
c.Session().Delete("name")
})
iris.Get("/clear", func(c *iris.Context) {
// removes all entries
c.Session().Clear()
})
iris.Get("/destroy", func(c *iris.Context) {
//destroy, removes the entire session and cookie
c.SessionDestroy()
})
println("Server is listening at :8080")
iris.Listen("8080")
}
```
## How to use - hard way
```go
// New creates & returns a new Manager and start its GC
// accepts 4 parameters
// first is the providerName (string) ["memory","redis"]
// second is the cookieName, the session's name (string) ["mysessionsecretcookieid"]
// third is the gcDuration (time.Duration)
// when this time passes it removes from
// temporary memory GC the value which hasn't be used for a long time(gcDuration)
// this is for the client's/browser's Cookie life time(expires) also
New(provider string, cName string, gcDuration time.Duration) *sessions.Manager
```
Example **memory**
```go
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
_ "github.com/kataras/iris/sessions/providers/memory" // here we add the memory provider and store
)
var sess *sessions.Manager
func init() {
sess = sessions.New("memory", "irissessionid", time.Duration(60)*time.Minute)
}
func main() {
iris.Get("/set", func(c *iris.Context) {
//get the session for this context
session := sess.Start(c)
//set session values
session.Set("name", "kataras")
//test if setted here
c.Write("All ok session setted to: %s", session.Get("name"))
})
iris.Get("/get", func(c *iris.Context) {
//get the session for this context
session := sess.Start(c)
var name string
//get the session value
if v := session.Get("name"); v != nil {
name = v.(string)
}
// OR just name = session.GetString("name")
c.Write("The name on the /set was: %s", name)
})
iris.Get("/delete", func(c *iris.Context) {
//get the session for this context
session := sess.Start(c)
session.Delete("name")
})
iris.Get("/clear", func(c *iris.Context) {
//get the session for this context
session := sess.Start(c)
// removes all entries
session.Clear()
})
iris.Get("/destroy", func(c *iris.Context) {
//destroy, removes the entire session and cookie
sess.Destroy(c)
})
iris.Listen("8080")
}
// session.GetAll() returns all values a map[interface{}]interface{}
// session.VisitAll(func(key interface{}, value interface{}) { /* loops for each entry */})
}
```
Example **redis** with default configuration
The default redis client points to 127.0.0.1:6379
```go
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
_ "github.com/kataras/iris/sessions/providers/redis"
// here we add the redis provider and store
//with the default redis client points to 127.0.0.1:6379
)
var sess *sessions.Manager
func init() {
sess = sessions.New("redis", "irissessionid", time.Duration(60)*time.Minute)
}
//... usage: same as memory
```
Example **redis** with custom configuration
```go
type Config struct {
// Network "tcp"
Network string
// Addr "127.0.01:6379"
Addr string
// Password string .If no password then no 'AUTH'. Default ""
Password string
// If Database is empty "" then no 'SELECT'. Default ""
Database string
// MaxIdle 0 no limit
MaxIdle int
// MaxActive 0 no limit
MaxActive int
// IdleTimeout 5 * time.Minute
IdleTimeout time.Duration
//Prefix "myprefix-for-this-website". Default ""
Prefix string
// MaxAgeSeconds how much long the redis should keep the session in seconds. Default 2520.0 (42minutes)
MaxAgeSeconds int
}
```
```go
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/providers/redis"
// here we add the redis provider and store
//with the default redis client points to 127.0.0.1:6379
)
var sess *sessions.Manager
func init() {
// you can config the redis after init also, but before any client's request
// but it's always a good idea to do it before sessions.New...
redis.Config.Network = "tcp"
redis.Config.Addr = "127.0.0.1:6379"
redis.Config.Prefix = "myprefix-for-this-website"
sess = sessions.New("redis", "irissessionid", time.Duration(60)*time.Minute)
}
//...usage: same as memory
```
### Security: Prevent session hijacking
> This section is external
**cookie only and token**
Through this simple example of hijacking a session, you can see that it's very dangerous because it allows attackers to do whatever they want. So how can we prevent session hijacking?
The first step is to only set session ids in cookies, instead of in URL rewrites. Also, we should set the httponly cookie property to true. This restricts client side scripts that want access to the session id. Using these techniques, cookies cannot be accessed by XSS and it won't be as easy as we showed to get a session id from a cookie manager.
The second step is to add a token to every request. Similar to the way we dealt with repeat forms in previous sections, we add a hidden field that contains a token. When a request is sent to the server, we can verify this token to prove that the request is unique.
```go
h := md5.New()
salt:="secret%^7&8888"
io.WriteString(h,salt+time.Now().String())
token:=fmt.Sprintf("%x",h.Sum(nil))
if r.Form["token"]!=token{
// ask to log in
}
session.Set("token",token)
```
**Session id timeout**
Another solution is to add a create time for every session, and to replace expired session ids with new ones. This can prevent session hijacking under certain circumstances.
```go
createtime := session.Get("createtime")
if createtime == nil {
session.Set("createtime", time.Now().Unix())
} else if (createtime.(int64) + 60) < (time.Now().Unix()) {
sess.Destroy(c)
session = sess.Start(c)
}
```
We set a value to save the create time and check if it's expired (I set 60 seconds here). This step can often thwart session hijacking attempts.
Combine the two solutions above and you will be able to prevent most session hijacking attempts from succeeding. On the one hand, session ids that are frequently reset will result in an attacker always getting expired and useless session ids; on the other hand, by setting the httponly property on cookies and ensuring that session ids can only be passed via cookies, all URL based attacks are mitigated.

View File

@ -1,14 +0,0 @@
package sessions
import (
"github.com/iris-contrib/errors"
)
var (
// ErrProviderNotFound returns an error with message: 'Provider was not found. Please try to _ import one'
ErrProviderNotFound = errors.New("Provider with name '%s' was not found. Please try to _ import this")
// ErrProviderRegister returns an error with message: 'On provider registration. Trace: nil or empty named provider are not acceptable'
ErrProviderRegister = errors.New("On provider registration. Trace: nil or empty named provider are not acceptable")
// ErrProviderAlreadyExists returns an error with message: 'On provider registration. Trace: provider with name '%s' already exists, maybe you register it twice'
ErrProviderAlreadyExists = errors.New("On provider registration. Trace: provider with name '%s' already exists, maybe you register it twice")
)

View File

@ -1,171 +0,0 @@
package sessions
import (
"encoding/base64"
"strings"
"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 {
config *config.Sessions
provider IProvider
mu sync.Mutex
}
)
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)
}
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)))
}
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))
}
var dotB = byte('.')
// Start starts the session
func (m *Manager) Start(ctx context.IContext) store.IStore {
m.mu.Lock()
var store store.IStore
requestCtx := ctx.GetRequestCtx()
cookieValue := string(requestCtx.Request.Header.Cookie(m.config.Cookie))
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()
// 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.SetKey(m.config.Cookie)
cookie.SetValue(sid)
cookie.SetPath("/")
if !m.config.DisableSubdomainPersistence {
requestDomain := ctx.HostString()
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
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
// so don't set a domain here
} else if strings.Count(requestDomain, ".") > 0 { // there is a problem with .localhost setted as the domain, so we check that first
// 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
}
}
// finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow)
cookie.SetDomain("." + requestDomain) // . to allow persistance
}
}
cookie.SetHTTPOnly(true)
cookie.SetExpire(m.config.Expires)
requestCtx.Response.Header.SetCookie(cookie)
fasthttp.ReleaseCookie(cookie)
} else {
store, _ = m.provider.Read(cookieValue)
}
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()
}

View File

@ -1,122 +0,0 @@
package sessions
import (
"container/list"
"sync"
"time"
"github.com/kataras/iris/sessions/store"
)
// IProvider the type which Provider must implement
type IProvider interface {
Name() string
Init(string) (store.IStore, error)
Read(string) (store.IStore, error)
Destroy(string) error
Update(string) error
GC(time.Duration)
}
type (
// Provider implements the IProvider
// contains the temp sessions memory, the store and some options for the cookies
Provider struct {
name string
mu sync.Mutex
sessions map[string]*list.Element // underline TEMPORARY memory store
list *list.List // for GC
NewStore func(sessionId string, cookieLifeDuration time.Duration) store.IStore
OnDestroy func(store store.IStore) // this is called when .Destroy
cookieLifeDuration time.Duration
}
)
var _ IProvider = &Provider{}
// NewProvider returns a new empty Provider
func NewProvider(name string) *Provider {
provider := &Provider{name: name, list: list.New()}
provider.sessions = make(map[string]*list.Element, 0)
return provider
}
// Init creates the store for the first time for this session and returns it
func (p *Provider) Init(sid string) (store.IStore, error) {
p.mu.Lock()
newSessionStore := p.NewStore(sid, p.cookieLifeDuration)
elem := p.list.PushBack(newSessionStore)
p.sessions[sid] = elem
p.mu.Unlock()
return newSessionStore, nil
}
// Read returns the store which sid parameter is belongs
func (p *Provider) Read(sid string) (store.IStore, error) {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
p.mu.Unlock() // yes defer is slow
return elem.Value.(store.IStore), nil
}
p.mu.Unlock()
// if not found
sessionStore, err := p.Init(sid)
return sessionStore, err
}
// Destroy always returns a nil error, for now.
func (p *Provider) Destroy(sid string) error {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
elem.Value.(store.IStore).Destroy()
delete(p.sessions, sid)
p.list.Remove(elem)
}
p.mu.Unlock()
return nil
}
// Update updates the lastAccessedTime, and moves the memory place element to the front
// always returns a nil error, for now
func (p *Provider) Update(sid string) error {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
elem.Value.(store.IStore).SetLastAccessedTime(time.Now())
p.list.MoveToFront(elem)
}
p.mu.Unlock()
return nil
}
// GC clears the memory
func (p *Provider) GC(duration time.Duration) {
p.mu.Lock()
p.cookieLifeDuration = duration
defer p.mu.Unlock() //let's defer it and trust the go
for {
elem := p.list.Back()
if elem == nil {
break
}
// if the time has passed. session was expired, then delete the session and its memory place
if (elem.Value.(store.IStore).LastAccessedTime().Unix() + duration.Nanoseconds()) < time.Now().Unix() {
p.list.Remove(elem)
delete(p.sessions, elem.Value.(store.IStore).ID())
} else {
break
}
}
}
// Name the provider's name, example: 'memory' or 'redis'
func (p *Provider) Name() string {
return p.name
}

View File

@ -1,27 +0,0 @@
package memory
import (
"time"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/store"
)
func init() {
register()
}
var (
// Provider the memory provider
Provider = sessions.NewProvider("memory")
)
// register registers itself (the new provider with its memory store) to the sessions providers
// must runs only once
func register() {
// the actual work is here.
Provider.NewStore = func(sessionId string, cookieLifeDuration time.Duration) store.IStore {
return &Store{sid: sessionId, lastAccessedTime: time.Now(), values: make(map[string]interface{}, 0)}
}
sessions.Register(Provider)
}

View File

@ -1,119 +0,0 @@
package memory
import (
"sync"
"time"
"github.com/kataras/iris/sessions/store"
)
// Store the memory store, contains the session id and the values
type Store struct {
sid string
lastAccessedTime time.Time
values map[string]interface{} // here is the real memory store
mu sync.Mutex
}
var _ store.IStore = &Store{}
// GetAll returns all values
func (s *Store) GetAll() map[string]interface{} {
return s.values
}
// VisitAll loop each one entry and calls the callback function func(key,value)
func (s *Store) VisitAll(cb func(k string, v interface{})) {
for key := range s.values {
cb(key, s.values[key])
}
}
// Get returns the value of an entry by its key
func (s *Store) Get(key string) interface{} {
Provider.Update(s.sid)
if value, found := s.values[key]; found {
return value
}
return nil
}
// GetString same as Get but returns as string, if nil then returns an empty string
func (s *Store) GetString(key string) string {
if value := s.Get(key); value != nil {
if v, ok := value.(string); ok {
return v
}
}
return ""
}
// GetInt same as Get but returns as int, if nil then returns -1
func (s *Store) GetInt(key string) int {
if value := s.Get(key); value != nil {
if v, ok := value.(int); ok {
return v
}
}
return -1
}
// Set fills the session with an entry, it receives a key and a value
// returns an error, which is always nil
func (s *Store) Set(key string, value interface{}) error {
s.mu.Lock()
s.values[key] = value
s.mu.Unlock()
Provider.Update(s.sid)
return nil
}
// Delete removes an entry by its key
// returns an error, which is always nil
func (s *Store) Delete(key string) error {
s.mu.Lock()
delete(s.values, key)
s.mu.Unlock()
Provider.Update(s.sid)
return nil
}
// Clear removes all entries
// returns an error, which is always nil
func (s *Store) Clear() error {
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
Provider.Update(s.sid)
return nil
}
// ID returns the session id
func (s *Store) ID() string {
return s.sid
}
// LastAccessedTime returns the last time this session has been used
func (s *Store) LastAccessedTime() time.Time {
return s.lastAccessedTime
}
// SetLastAccessedTime updates the last accessed time
func (s *Store) SetLastAccessedTime(lastacc time.Time) {
s.lastAccessedTime = lastacc
}
// Destroy deletes all keys
func (s *Store) Destroy() {
// clears without provider's update.
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
}

View File

@ -1,188 +0,0 @@
package redis
import (
"sync"
"time"
"github.com/kataras/iris/sessions/store"
"github.com/kataras/iris/utils"
)
/*Notes only for me
--------
Here we are setting a structure which keeps the current session's values setted by store.Set(key,value)
this is the RedisValue struct.
if noexists
RedisValue := RedisValue{sessionid,values)
RedisValue.values[thekey]=thevalue
service.Set(store.sid,RedisValue)
because we are using the same redis service for all sessions, and this is the best way to separate them,
without prefix and all that which I tried and failed to deserialize them correctly if the value is string...
so again we will keep the current server's sessions into memory
and fetch them(the sessions) from the redis at each first session run. Yes this is the fastest way to get/set a session
and at the same time they are keep saved to the redis and the GC will cleanup the memory after a while like we are doing
with the memory provider. Or just have a values field inside the Store and use just it, yes better simpler approach.
Ok then, let's convert it again.
*/
// Values is just a type of a map[string]interface{}
type Values map[string]interface{}
// Store the redis session store
type Store struct {
sid string
lastAccessedTime time.Time
values Values
cookieLifeDuration time.Duration //used on .Set-> SETEX on redis
mu sync.Mutex
}
var _ store.IStore = &Store{}
// NewStore creates and returns a new store based on the session id(string) and the cookie life duration (time.Duration)
func NewStore(sid string, cookieLifeDuration time.Duration) *Store {
s := &Store{sid: sid, lastAccessedTime: time.Now(), cookieLifeDuration: cookieLifeDuration}
//fetch the values from this session id and copy-> store them
val, err := redis.GetBytes(sid)
if err == nil {
err = utils.DeserializeBytes(val, &s.values)
if err != nil {
//if deserialization failed
s.values = Values{}
}
}
if s.values == nil {
//if key/sid wasn't found or was found but no entries in it(L72)
s.values = Values{}
}
return s
}
// serialize the values to be stored as strings inside the Redis, we panic at any serialization error here
func serialize(values Values) []byte {
val, err := utils.SerializeBytes(values)
if err != nil {
panic("On redisstore.serialize: " + err.Error())
}
return val
}
// update updates the real redis store
func (s *Store) update() {
go redis.Set(s.sid, serialize(s.values), s.cookieLifeDuration.Seconds()) //set/update all the values, in goroutine
}
// GetAll returns all values
func (s *Store) GetAll() map[string]interface{} {
return s.values
}
// VisitAll loop each one entry and calls the callback function func(key,value)
func (s *Store) VisitAll(cb func(k string, v interface{})) {
for key := range s.values {
cb(key, s.values[key])
}
}
// Get returns the value of an entry by its key
func (s *Store) Get(key string) interface{} {
Provider.Update(s.sid)
if value, found := s.values[key]; found {
return value
}
return nil
}
// GetString same as Get but returns as string, if nil then returns an empty string
func (s *Store) GetString(key string) string {
if value := s.Get(key); value != nil {
if v, ok := value.(string); ok {
return v
}
}
return ""
}
// GetInt same as Get but returns as int, if nil then returns -1
func (s *Store) GetInt(key string) int {
if value := s.Get(key); value != nil {
if v, ok := value.(int); ok {
return v
}
}
return -1
}
// Set fills the session with an entry, it receives a key and a value
// returns an error, which is always nil
func (s *Store) Set(key string, value interface{}) error {
s.mu.Lock()
s.values[key] = value
s.mu.Unlock()
Provider.Update(s.sid)
s.update()
return nil
}
// Delete removes an entry by its key
// returns an error, which is always nil
func (s *Store) Delete(key string) error {
s.mu.Lock()
delete(s.values, key)
s.mu.Unlock()
Provider.Update(s.sid)
s.update()
return nil
}
// Clear removes all entries
// returns an error, which is always nil
func (s *Store) Clear() error {
//we are not using the Redis.Delete, I made so work for nothing.. we wanted only the .Set at the end...
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
Provider.Update(s.sid)
s.update()
return nil
}
// ID returns the session id
func (s *Store) ID() string {
return s.sid
}
// LastAccessedTime returns the last time this session has been used
func (s *Store) LastAccessedTime() time.Time {
return s.lastAccessedTime
}
// SetLastAccessedTime updates the last accessed time
func (s *Store) SetLastAccessedTime(lastacc time.Time) {
s.lastAccessedTime = lastacc
}
// Destroy deletes entirely the session, from the memory, the client's cookie and the store
func (s *Store) Destroy() {
// remove the whole value which is the s.values from real redis
redis.Delete(s.sid)
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
}

View File

@ -1,48 +0,0 @@
package redis
import (
"time"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/providers/redis/service"
"github.com/kataras/iris/sessions/store"
)
func init() {
register()
}
var (
// Provider is the redis provider
Provider = sessions.NewProvider("redis")
// redis is the default redis service, you can set configs via this object
redis = service.New()
// Config is just the Redis(service)' config
Config = redis.Config
// Empty() because maybe the user wants to edit the default configs.
//the Connect goes to the first NewStore, when user ask for session, so you have the time to change the default configs
)
// register registers itself (the new provider with its memory store) to the sessions providers
// must runs only once
func register() {
// the actual work is here.
Provider.NewStore = func(sessionId string, cookieLifeDuration time.Duration) store.IStore {
//println("memory.go:49-> requesting new memory store with sessionid: " + sessionId)
if !redis.Connected {
redis.Connect()
_, err := redis.PingPong()
if err != nil {
if err != nil {
// don't use to get the logger, just prin these to the console... atm
println("Redis Connection error on iris/sessions/providers/redisstore.Connect: " + err.Error())
println("But don't panic, auto-switching to memory store right now!")
}
}
}
return NewStore(sessionId, cookieLifeDuration)
}
sessions.Register(Provider)
}

View File

@ -1,279 +0,0 @@
package service
import (
"time"
"github.com/garyburd/redigo/redis"
"github.com/iris-contrib/errors"
"github.com/kataras/iris/config"
)
var (
// ErrRedisClosed an error with message 'Redis is already closed'
ErrRedisClosed = errors.New("Redis is already closed")
// ErrKeyNotFound an error with message 'Key $thekey doesn't found'
ErrKeyNotFound = errors.New("Key '%s' doesn't found")
)
// Service the Redis service, contains the config and the redis pool
type Service struct {
// Connected is true when the Service has already connected
Connected bool
// Config the redis config for this redis
Config *config.Redis
pool *redis.Pool
}
// PingPong sends a ping and receives a pong, if no pong received then returns false and filled error
func (r *Service) PingPong() (bool, error) {
c := r.pool.Get()
defer c.Close()
msg, err := c.Do("PING")
if err != nil || msg == nil {
return false, err
}
return (msg == "PONG"), nil
}
// CloseConnection closes the redis connection
func (r *Service) CloseConnection() error {
if r.pool != nil {
return r.pool.Close()
}
return ErrRedisClosed.Return()
}
// Set sets to the redis
// key string, value string, you can use utils.Serialize(&myobject{}) to convert an object to []byte
func (r *Service) Set(key string, value []byte, maxageseconds ...float64) (err error) { // map[interface{}]interface{}) (err error) {
maxage := config.DefaultRedisMaxAgeSeconds //1 year
c := r.pool.Get()
defer c.Close()
if err = c.Err(); err != nil {
return
}
if len(maxageseconds) > 0 {
if max := maxageseconds[0]; max >= 0 {
maxage = max
}
}
_, err = c.Do("SETEX", r.Config.Prefix+key, maxage, value)
return
}
// Get returns value, err by its key
// you can use utils.Deserialize((.Get("yourkey"),&theobject{})
//returns nil and a filled error if something wrong happens
func (r *Service) Get(key string) (interface{}, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
redisVal, err := c.Do("GET", r.Config.Prefix+key)
if err != nil {
return nil, err
}
if redisVal == nil {
return nil, ErrKeyNotFound.Format(key)
}
return redisVal, nil
}
// GetBytes returns value, err by its key
// you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{})
//returns nil and a filled error if something wrong happens
func (r *Service) GetBytes(key string) ([]byte, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
redisVal, err := c.Do("GET", r.Config.Prefix+key)
if err != nil {
return nil, err
}
if redisVal == nil {
return nil, ErrKeyNotFound.Format(key)
}
return redis.Bytes(redisVal, err)
}
// GetString returns value, err by its key
// you can use utils.Deserialize((.GetString("yourkey"),&theobject{})
//returns empty string and a filled error if something wrong happens
func (r *Service) GetString(key string) (string, error) {
redisVal, err := r.Get(key)
if redisVal == nil {
return "", ErrKeyNotFound.Format(key)
}
sVal, err := redis.String(redisVal, err)
if err != nil {
return "", err
}
return sVal, nil
}
// GetInt returns value, err by its key
// you can use utils.Deserialize((.GetInt("yourkey"),&theobject{})
//returns -1 int and a filled error if something wrong happens
func (r *Service) GetInt(key string) (int, error) {
redisVal, err := r.Get(key)
if redisVal == nil {
return -1, ErrKeyNotFound.Format(key)
}
intVal, err := redis.Int(redisVal, err)
if err != nil {
return -1, err
}
return intVal, nil
}
// GetStringMap returns map[string]string, err by its key
//returns nil and a filled error if something wrong happens
func (r *Service) GetStringMap(key string) (map[string]string, error) {
redisVal, err := r.Get(key)
if redisVal == nil {
return nil, ErrKeyNotFound.Format(key)
}
_map, err := redis.StringMap(redisVal, err)
if err != nil {
return nil, err
}
return _map, nil
}
// GetAll returns all keys and their values from a specific key (map[string]string)
// returns a filled error if something bad happened
func (r *Service) GetAll(key string) (map[string]string, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
reply, err := c.Do("HGETALL", r.Config.Prefix+key)
if err != nil {
return nil, err
}
if reply == nil {
return nil, ErrKeyNotFound.Format(key)
}
return redis.StringMap(reply, err)
}
// GetAllKeysByPrefix returns all []string keys by a key prefix from the redis
func (r *Service) GetAllKeysByPrefix(prefix string) ([]string, error) {
c := r.pool.Get()
defer c.Close()
if err := c.Err(); err != nil {
return nil, err
}
reply, err := c.Do("KEYS", r.Config.Prefix+prefix)
if err != nil {
return nil, err
}
if reply == nil {
return nil, ErrKeyNotFound.Format(prefix)
}
return redis.Strings(reply, err)
}
// Delete removes redis entry by specific key
func (r *Service) Delete(key string) error {
c := r.pool.Get()
defer c.Close()
if _, err := c.Do("DEL", r.Config.Prefix+key); err != nil {
return err
}
return nil
}
func dial(network string, addr string, pass string) (redis.Conn, error) {
if network == "" {
network = config.DefaultRedisNetwork
}
if addr == "" {
addr = config.DefaultRedisAddr
}
c, err := redis.Dial(network, addr)
if err != nil {
return nil, err
}
if pass != "" {
if _, err = c.Do("AUTH", pass); err != nil {
c.Close()
return nil, err
}
}
return c, err
}
// Connect connects to the redis, called only once
func (r *Service) Connect() {
c := r.Config
if c.IdleTimeout <= 0 {
c.IdleTimeout = config.DefaultRedisIdleTimeout
}
if c.Network == "" {
c.Network = config.DefaultRedisNetwork
}
if c.Addr == "" {
c.Addr = config.DefaultRedisAddr
}
if c.MaxAgeSeconds <= 0 {
c.MaxAgeSeconds = config.DefaultRedisMaxAgeSeconds
}
pool := &redis.Pool{IdleTimeout: config.DefaultRedisIdleTimeout, MaxIdle: c.MaxIdle, MaxActive: c.MaxActive}
pool.TestOnBorrow = func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
}
if c.Database != "" {
pool.Dial = func() (redis.Conn, error) {
red, err := dial(c.Network, c.Addr, c.Password)
if err != nil {
return nil, err
}
if _, err = red.Do("SELECT", c.Database); err != nil {
red.Close()
return nil, err
}
return red, err
}
} else {
pool.Dial = func() (redis.Conn, error) {
return dial(c.Network, c.Addr, c.Password)
}
}
r.Connected = true
r.pool = pool
}
// New returns a Redis service filled by the passed config
// to connect call the .Connect()
func New(cfg ...config.Redis) *Service {
c := config.DefaultRedis().Merge(cfg)
r := &Service{pool: &redis.Pool{}, Config: &c}
return r
}

View File

@ -1,19 +0,0 @@
package sessions
import "github.com/kataras/iris/config"
// New creates & returns a new Manager and start its GC
func New(cfg ...config.Sessions) *Manager {
c := config.DefaultSessions().Merge(cfg)
// If provider is empty then return nil manager, means that the sessions are disabled
if c.Provider == "" {
return nil
}
manager, err := newManager(c)
if err != nil {
panic(err.Error()) // we have to panic here because we will start GC after and if provider is nil then many panics will come
}
//run the GC here
go manager.GC()
return manager
}

View File

@ -1,20 +0,0 @@
// Package store the package is in diffent folder to reduce the import cycles from the ./context/context.go *
package store
import "time"
// IStore is the interface which all session stores should implement
type IStore interface {
Get(string) interface{}
GetString(string) string
GetInt(string) int
Set(string, interface{}) error
Delete(string) error
Clear() error
VisitAll(func(string, interface{}))
GetAll() map[string]interface{}
ID() string
LastAccessedTime() time.Time
SetLastAccessedTime(time.Time)
Destroy()
}

282
template.go Normal file
View File

@ -0,0 +1,282 @@
package iris
import (
"compress/gzip"
"io"
"path/filepath"
"sync"
"github.com/iris-contrib/errors"
"github.com/kataras/iris/utils"
)
var (
builtinFuncs = [...]string{"url", "urlpath"}
// DefaultTemplateDirectory the default directory if empty setted
DefaultTemplateDirectory = "." + utils.PathSeparator + "templates"
)
var (
// ContentTypeHTML the content type header for rendering
// this can be changed
ContentTypeHTML = "text/html"
// Charset the charset header for rendering
// this can be changed
Charset = "UTF-8"
)
const (
// DefaultTemplateExtension the default file extension if empty setted
DefaultTemplateExtension = ".html"
// NoLayout to disable layout for a particular template file
NoLayout = "@.|.@iris_no_layout@.|.@"
// TemplateLayoutContextKey is the name of the user values which can be used to set a template layout from a middleware and override the parent's
TemplateLayoutContextKey = "templateLayout"
)
type (
// TemplateEngine the interface that all template engines must implement
TemplateEngine interface {
// LoadDirectory builds the templates, usually by directory and extension but these are engine's decisions
LoadDirectory(directory string, extension string) error
// LoadAssets loads the templates by binary
// assetFn is a func which returns bytes, use it to load the templates by binary
// namesFn returns the template filenames
LoadAssets(virtualDirectory string, virtualExtension string, assetFn func(name string) ([]byte, error), namesFn func() []string) error
// ExecuteWriter finds, execute a template and write its result to the out writer
// options are the optional runtime options can be passed by user and catched by the template engine when render
// an example of this is the "layout" or "gzip" option
ExecuteWriter(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) error
}
// TemplateEngineFuncs is optional interface for the TemplateEngine
// used to insert the Iris' standard funcs, see var 'usedFuncs'
TemplateEngineFuncs interface {
// Funcs should returns the context or the funcs,
// this property is used in order to register the iris' helper funcs
Funcs() map[string]interface{}
}
)
type (
// TemplateFuncs is is a helper type for map[string]interface{}
TemplateFuncs map[string]interface{}
// RenderOptions is a helper type for the optional runtime options can be passed by user when Render
// an example of this is the "layout" or "gzip" option
// same as Map but more specific name
RenderOptions map[string]interface{}
)
// IsFree returns true if a function can be inserted to this map
// return false if this key is already used by Iris
func (t TemplateFuncs) IsFree(key string) bool {
for i := range builtinFuncs {
if builtinFuncs[i] == key {
return false
}
}
return true
}
type (
// TemplateEngineLocation contains the funcs to set the location for the templates by directory or by binary
TemplateEngineLocation struct {
directory string
extension string
assetFn func(name string) ([]byte, error)
namesFn func() []string
}
// TemplateEngineBinaryLocation called after TemplateEngineLocation's Directory, used when files are distrubuted inside the app executable
TemplateEngineBinaryLocation struct {
location *TemplateEngineLocation
}
)
// Directory sets the directory to load from
// returns the Binary location which is optional
func (t *TemplateEngineLocation) Directory(dir string, fileExtension string) TemplateEngineBinaryLocation {
t.directory = dir
t.extension = fileExtension
return TemplateEngineBinaryLocation{location: t}
}
// Binary sets the asset(s) and asssets names to load from, works with Directory
func (t *TemplateEngineBinaryLocation) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) {
t.location.assetFn = assetFn
t.location.namesFn = namesFn
// if extension is not static(setted by .Directory)
if t.location.extension == "" {
if names := namesFn(); len(names) > 0 {
t.location.extension = filepath.Ext(names[0]) // we need the extension to get the correct template engine on the Render method
}
}
}
func (t *TemplateEngineLocation) isBinary() bool {
return t.assetFn != nil && t.namesFn != nil
}
// TemplateEngineWrapper is the wrapper of a template engine
type TemplateEngineWrapper struct {
TemplateEngine
location *TemplateEngineLocation
buffer *utils.BufferPool
gzipWriterPool sync.Pool
reload bool
combiledContentType string
}
var (
errMissingDirectoryOrAssets = errors.New("Missing Directory or Assets by binary for the template engine!")
errNoTemplateEngineForExt = errors.New("No template engine found to manage '%s' extensions")
)
func (t *TemplateEngineWrapper) load() error {
if t.location.isBinary() {
t.LoadAssets(t.location.directory, t.location.extension, t.location.assetFn, t.location.namesFn)
} else if t.location.directory != "" {
t.LoadDirectory(t.location.directory, t.location.extension)
} else {
return errMissingDirectoryOrAssets.Return()
}
return nil
}
// Execute execute a template and write its result to the context's body
// options are the optional runtime options can be passed by user and catched by the template engine when render
// an example of this is the "layout"
// note that gzip option is an iris dynamic option which exists for all template engines
func (t *TemplateEngineWrapper) Execute(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) (err error) {
if t == nil {
//file extension, but no template engine registered, this caused by context, and TemplateEngines. GetBy
return errNoTemplateEngineForExt.Format(filepath.Ext(filename))
}
if t.reload {
if err = t.load(); err != nil {
return
}
}
// we do all these because we don't want to initialize a new map for each execution...
gzipEnabled := false
if len(options) > 0 {
gzipOpt := options[0]["gzip"] // we only need that, so don't create new map to keep the options.
if b, isBool := gzipOpt.(bool); isBool {
gzipEnabled = b
}
}
ctxLayout := ctx.GetString(TemplateLayoutContextKey)
if ctxLayout != "" {
if len(options) > 0 {
options[0]["layout"] = ctxLayout
} else {
options = []map[string]interface{}{map[string]interface{}{"layout": ctxLayout}}
}
}
var out io.Writer
if gzipEnabled {
ctx.Response.Header.Add("Content-Encoding", "gzip")
gzipWriter := t.gzipWriterPool.Get().(*gzip.Writer)
gzipWriter.Reset(ctx.Response.BodyWriter())
defer gzipWriter.Close()
defer t.gzipWriterPool.Put(gzipWriter)
out = gzipWriter
} else {
out = ctx.Response.BodyWriter()
}
ctx.SetHeader("Content-Type", t.combiledContentType)
return t.ExecuteWriter(out, filename, binding, options...)
}
// ExecuteToString executes a template from a specific template engine and returns its contents result as string, it doesn't renders
func (t *TemplateEngineWrapper) ExecuteToString(filename string, binding interface{}, opt ...map[string]interface{}) (result string, err error) {
if t == nil {
//file extension, but no template engine registered, this caused by context, and TemplateEngines. GetBy
return "", errNoTemplateEngineForExt.Format(filepath.Ext(filename))
}
if t.reload {
if err = t.load(); err != nil {
return
}
}
out := t.buffer.Get()
defer t.buffer.Put(out)
err = t.ExecuteWriter(out, filename, binding, opt...)
if err == nil {
result = out.String()
}
return
}
// TemplateEngines is the container and manager of the template engines
type TemplateEngines struct {
Helpers map[string]interface{}
Engines []*TemplateEngineWrapper
Reload bool
}
// GetBy receives a filename, gets its extension and returns the template engine responsible for that file extension
func (t *TemplateEngines) GetBy(filename string) *TemplateEngineWrapper {
extension := filepath.Ext(filename)
for i, n := 0, len(t.Engines); i < n; i++ {
e := t.Engines[i]
if e.location.extension == extension {
return e
}
}
return nil
}
// Add adds but not loads a template engine
func (t *TemplateEngines) Add(e TemplateEngine) *TemplateEngineLocation {
location := &TemplateEngineLocation{}
// add the iris helper funcs
if funcer, ok := e.(TemplateEngineFuncs); ok {
if funcer.Funcs() != nil {
for k, v := range t.Helpers {
funcer.Funcs()[k] = v
}
}
}
tmplEngine := &TemplateEngineWrapper{
TemplateEngine: e,
location: location,
buffer: utils.NewBufferPool(20),
gzipWriterPool: sync.Pool{New: func() interface{} {
return &gzip.Writer{}
}},
reload: t.Reload,
combiledContentType: ContentTypeHTML + "; " + Charset,
}
t.Engines = append(t.Engines, tmplEngine)
return location
}
// LoadAll loads all templates using all template engines, returns the first error
// called on iris' initialize
func (t *TemplateEngines) LoadAll() error {
for i, n := 0, len(t.Engines); i < n; i++ {
e := t.Engines[i]
if e.location.directory == "" {
e.location.directory = DefaultTemplateDirectory // the defualt dir ./templates
}
if e.location.extension == "" {
e.location.extension = DefaultTemplateExtension // the default file ext .html
}
if err := e.load(); err != nil {
return err
}
}
return nil
}