mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
🌈 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:
parent
af4df18ec4
commit
077984bd60
24
HISTORY.md
24
HISTORY.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
39
context.go
39
context.go
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
63
iris.go
|
@ -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
355
sessions.go
Normal 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()
|
||||
})
|
||||
}
|
|
@ -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.
|
|
@ -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")
|
||||
)
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
282
template.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user