From 13e83fc57e2921b388518bf6ec0f863f90c277aa Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Wed, 15 Feb 2017 20:06:19 +0200 Subject: [PATCH] SessionsPolicy and sessions adaptor, history and _example written. Former-commit-id: e8b0dde3cb3b72919f01b9d836d8ccb3d4e20214 --- HISTORY.md | 115 ++++++- adaptors/sessions/LICENSE | 21 ++ adaptors/sessions/_example/main.go | 87 ++++++ adaptors/sessions/config.go | 74 +++++ adaptors/sessions/cookie.go | 165 ++++++++++ adaptors/sessions/database.go | 17 + adaptors/sessions/provider.go | 126 ++++++++ adaptors/sessions/session.go | 294 ++++++++++++++++++ adaptors/sessions/sessiondb/README.md | 29 ++ adaptors/sessions/sessiondb/redis/database.go | 85 +++++ .../sessiondb/redis/service/config.go | 78 +++++ .../sessiondb/redis/service/service.go | 272 ++++++++++++++++ adaptors/sessions/sessions.go | 188 +++++++++++ adaptors/typescript/typescript.go | 5 + adaptors/view/adaptor.go | 2 + adaptors/websocket/websocket.go | 1 + configuration.go | 86 ----- context.go | 73 ++++- iris.go | 54 +--- policy.go | 95 ++++++ router.go | 3 + 21 files changed, 1729 insertions(+), 141 deletions(-) create mode 100644 adaptors/sessions/LICENSE create mode 100644 adaptors/sessions/_example/main.go create mode 100644 adaptors/sessions/config.go create mode 100644 adaptors/sessions/cookie.go create mode 100644 adaptors/sessions/database.go create mode 100644 adaptors/sessions/provider.go create mode 100644 adaptors/sessions/session.go create mode 100644 adaptors/sessions/sessiondb/README.md create mode 100644 adaptors/sessions/sessiondb/redis/database.go create mode 100644 adaptors/sessions/sessiondb/redis/service/config.go create mode 100644 adaptors/sessions/sessiondb/redis/service/service.go create mode 100644 adaptors/sessions/sessions.go diff --git a/HISTORY.md b/HISTORY.md index f7f25ff3..9e471d8b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -855,8 +855,121 @@ editors worked before but I couldn't let some developers without support. ### Sessions -- IMPROVEMENT: [Sessions manager](https://github.com/kataras/go-sessions) works even faster now. +Sessions manager is also an Adaptor now, `iris.SessionsPolicy`. +So far we used the `kataras/go-sessions`, you could always use other session manager ofcourse but you would lose the `context.Session()` +and its returning value, the `iris.Session` now. + +`SessionsPolicy` gives the developers the opportunity to adapt any, +compatible with a particular simple interface(Start and Destroy methods), third-party sessions managers. + +- The API for sessions inside context is the same, no matter what session manager you wanna to adapt. +- The API for sessions inside context didn't changed, it's the same as you knew it. + +- Iris, of course, has built'n `SessionsPolicy` adaptor(the kataras/go-sessions: edited to remove fasthttp dependencies). + - Sessions manager works even faster now and a bug fixed for some browsers. + +- Functions like, adding a database or store(i.e: `UseDatabase`) depends on the session manager of your choice, +Iris doesn't requires these things +to adapt a package as a session manager. So `iris.UseDatabase` has been removed and depends on the `mySessions.UseDatabase` you 'll see below. + +- `iris.DestroySessionByID and iris.DestroyAllSessions` have been also removed, depends on the session manager of your choice, `mySessions.DestroyByID and mySessions.DestroyAll` should do the job now. + + +> Don't worry about forgetting to adapt any feature that you use inside Iris, Iris will print you a how-to-fix message at iris.DevMode log level. + +**[Example](https://github.com/kataras/iris/tree/6.2/adaptors/sessions/_example) code:** + +```go +package main + +import ( + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/sessions" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + + mySessions := sessions.New(sessions.Config{ + // Cookie string, the session's client cookie name, for example: "mysessionid" + // + // Defaults to "irissessionid" + Cookie: "mysessionid", + // base64 urlencoding, + // if you have strange name cookie name enable this + DecodeCookie: false, + // it's time.Duration, from the time cookie is created, how long it can be alive? + // 0 means no expire. + // -1 means expire when browser closes + // or set a value, like 2 hours: + Expires: time.Hour * 2, + // the length of the sessionid's cookie's value + CookieLength: 32, + // if you want to invalid cookies on different subdomains + // of the same host, then enable it + DisableSubdomainPersistence: false, + }) + + // OPTIONALLY: + // import "gopkg.in/kataras/iris.v6/adaptors/sessions/sessiondb/redis" + // or import "github.com/kataras/go-sessions/sessiondb/$any_available_community_database" + // mySessions.UseDatabase(redis.New(...)) + + app.Adapt(mySessions) // Adapt the session manager we just created. + + app.Get("/", func(ctx *iris.Context) { + ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") + }) + app.Get("/set", func(ctx *iris.Context) { + + //set session values + ctx.Session().Set("name", "iris") + + //test if setted here + ctx.Writef("All ok session setted to: %s", ctx.Session().GetString("name")) + }) + + app.Get("/get", func(ctx *iris.Context) { + // get a specific key, as string, if no found returns just an empty string + name := ctx.Session().GetString("name") + + ctx.Writef("The name on the /set was: %s", name) + }) + + app.Get("/delete", func(ctx *iris.Context) { + // delete a specific key + ctx.Session().Delete("name") + }) + + app.Get("/clear", func(ctx *iris.Context) { + // removes all entries + ctx.Session().Clear() + }) + + app.Get("/destroy", func(ctx *iris.Context) { + + //destroy, removes the entire session and cookie + ctx.SessionDestroy() + msg := "You have to refresh the page to completely remove the session (browsers works this way, it's not iris-specific.)" + + ctx.Writef(msg) + ctx.Log(iris.DevMode, msg) + }) // Note about destroy: + // + // You can destroy a session outside of a handler too, using the: + // mySessions.DestroyByID + // mySessions.DestroyAll + + app.Listen(":8080") +} + +``` ### Websockets diff --git a/adaptors/sessions/LICENSE b/adaptors/sessions/LICENSE new file mode 100644 index 00000000..2935ad5d --- /dev/null +++ b/adaptors/sessions/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/adaptors/sessions/_example/main.go b/adaptors/sessions/_example/main.go new file mode 100644 index 00000000..09cd67a0 --- /dev/null +++ b/adaptors/sessions/_example/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/sessions" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) // enable all (error) logs + app.Adapt(httprouter.New()) // select the httprouter as the servemux + + mySessions := sessions.New(sessions.Config{ + // Cookie string, the session's client cookie name, for example: "mysessionid" + // + // Defaults to "irissessionid" + Cookie: "mysessionid", + // base64 urlencoding, + // if you have strange name cookie name enable this + DecodeCookie: false, + // it's time.Duration, from the time cookie is created, how long it can be alive? + // 0 means no expire. + // -1 means expire when browser closes + // or set a value, like 2 hours: + Expires: time.Hour * 2, + // the length of the sessionid's cookie's value + CookieLength: 32, + // if you want to invalid cookies on different subdomains + // of the same host, then enable it + DisableSubdomainPersistence: false, + }) + + // OPTIONALLY: + // import "gopkg.in/kataras/iris.v6/adaptors/sessions/sessiondb/redis" + // or import "github.com/kataras/go-sessions/sessiondb/$any_available_community_database" + // mySessions.UseDatabase(redis.New(...)) + + app.Adapt(mySessions) // Adapt the session manager we just created. + + app.Get("/", func(ctx *iris.Context) { + ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") + }) + app.Get("/set", func(ctx *iris.Context) { + + //set session values + ctx.Session().Set("name", "iris") + + //test if setted here + ctx.Writef("All ok session setted to: %s", ctx.Session().GetString("name")) + }) + + app.Get("/get", func(ctx *iris.Context) { + // get a specific key, as string, if no found returns just an empty string + name := ctx.Session().GetString("name") + + ctx.Writef("The name on the /set was: %s", name) + }) + + app.Get("/delete", func(ctx *iris.Context) { + // delete a specific key + ctx.Session().Delete("name") + }) + + app.Get("/clear", func(ctx *iris.Context) { + // removes all entries + ctx.Session().Clear() + }) + + app.Get("/destroy", func(ctx *iris.Context) { + + //destroy, removes the entire session and cookie + ctx.SessionDestroy() + msg := "You have to refresh the page to completely remove the session (browsers works this way, it's not iris-specific.)" + + ctx.Writef(msg) + ctx.Log(iris.DevMode, msg) + }) // Note about destroy: + // + // You can destroy a session outside of a handler too, using the: + // mySessions.DestroyByID + // mySessions.DestroyAll + + app.Listen(":8080") +} diff --git a/adaptors/sessions/config.go b/adaptors/sessions/config.go new file mode 100644 index 00000000..55e5ef7f --- /dev/null +++ b/adaptors/sessions/config.go @@ -0,0 +1,74 @@ +package sessions + +import ( + "encoding/base64" + "time" +) + +const ( + // DefaultCookieName the secret cookie's name for sessions + DefaultCookieName = "irissessionid" + // DefaultCookieLength is the default Session Manager's CookieLength, which is 32 + DefaultCookieLength = 32 +) + +type ( + // Config is the configuration for sessions + // 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 cookie length (sessionid) int, defaults to 32, do not change if you don't have any reason to do + // fifth is the DisableSubdomainPersistence which you can set it to true in order dissallow your q subdomains to have access to the session cook + Config struct { + // Cookie string, the session's client cookie name, for example: "mysessionid" + // + // Defaults to "irissessionid" + Cookie string + + // DecodeCookie set it to true to decode the cookie key with base64 URLEncoding + // + // Defaults to false + DecodeCookie bool + + // Expires the duration of which the cookie must expires (created_time.Add(Expires)). + // If you want to delete the cookie when the browser closes, set it to -1. + // + // 0 means no expire, (24 years) + // -1 means when browser closes + // > 0 is the time.Duration which the session cookies should expire. + // + // Defaults to infinitive/unlimited life duration(0) + Expires time.Duration + + // CookieLength the length of the sessionid's cookie's value, let it to 0 if you don't want to change it + // + // Defaults to 32 + CookieLength int + + // DisableSubdomainPersistence set it to true in order dissallow your q subdomains to have access to the session cookie + // + // Defaults to false + DisableSubdomainPersistence bool + } +) + +// Validate corrects missing fields configuration fields and returns the right configuration +func (c Config) Validate() Config { + + if c.Cookie == "" { + c.Cookie = DefaultCookieName + } + + 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(Sessions.Cookie))) + } + + if c.CookieLength <= 0 { + c.CookieLength = DefaultCookieLength + } + + return c +} diff --git a/adaptors/sessions/cookie.go b/adaptors/sessions/cookie.go new file mode 100644 index 00000000..804a14be --- /dev/null +++ b/adaptors/sessions/cookie.go @@ -0,0 +1,165 @@ +package sessions + +import ( + "bytes" + "encoding/base64" + "encoding/gob" + "math/rand" + "net/http" + "strconv" + "strings" + "time" +) + +var ( + // CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie. + CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + + // CookieExpireUnlimited indicates that the cookie doesn't expire. + CookieExpireUnlimited = time.Now().AddDate(24, 10, 10) +) + +// GetCookie returns cookie's value by it's name +// returns empty string if nothing was found +func GetCookie(name string, req *http.Request) string { + c, err := req.Cookie(name) + if err != nil { + return "" + } + return c.Value +} + +// AddCookie adds a cookie +func AddCookie(cookie *http.Cookie, res http.ResponseWriter) { + if v := cookie.String(); v != "" { + http.SetCookie(res, cookie) + } +} + +// RemoveCookie deletes a cookie by it's name/key +func RemoveCookie(name string, res http.ResponseWriter, req *http.Request) { + c, err := req.Cookie(name) + if err != nil { + return + } + + c.Expires = CookieExpireDelete + // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' + c.MaxAge = -1 + c.Value = "" + c.Path = "/" + AddCookie(c, res) +} + +// IsValidCookieDomain returns true if the receiver is a valid domain to set +// valid means that is recognised as 'domain' by the browser, so it(the cookie) can be shared with subdomains also +func IsValidCookieDomain(domain string) bool { + if domain == "0.0.0.0" || domain == "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 mysubdomain.32.196.56.181. as scorrectly ubdomains because of the many dots + // so don't set a cookie domain here, let browser handle this + return false + } + + dotLen := strings.Count(domain, ".") + if dotLen == 0 { + // we don't have a domain, maybe something like 'localhost', browser doesn't see the .localhost as wildcard subdomain+domain + return false + } + if dotLen >= 3 { + if lastDotIdx := strings.LastIndexByte(domain, '.'); lastDotIdx != -1 { + // chekc the last part, if it's number then propably it's ip + if len(domain) > lastDotIdx+1 { + _, err := strconv.Atoi(domain[lastDotIdx+1:]) + if err == nil { + return false + } + } + } + } + + return true +} + +func encodeCookieValue(value string) string { + return base64.URLEncoding.EncodeToString([]byte(value)) +} + +func decodeCookieValue(value string) (string, error) { + v, err := base64.URLEncoding.DecodeString(value) + if err != nil { + return "", err + } + return string(v), nil +} + +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- +// ----------------------------------Strings & Serialization---------------------------- +// ------------------------------------------------------------------------------------- +// ------------------------------------------------------------------------------------- + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return b +} + +// randomString accepts a number(10 for example) and returns a random string using simple but fairly safe random algorithm +func randomString(n int) string { + return string(random(n)) +} + +// Serialize serialize any type to gob bytes and after returns its the base64 encoded string +func Serialize(m interface{}) (string, error) { + b := bytes.Buffer{} + encoder := gob.NewEncoder(&b) + err := encoder.Encode(m) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(b.Bytes()), nil +} + +// Deserialize accepts an encoded string and a data struct which will be filled with the desierialized string +// using gob decoder +func Deserialize(str string, m interface{}) error { + by, err := base64.StdEncoding.DecodeString(str) + if err != nil { + return err + } + b := bytes.Buffer{} + b.Write(by) + d := gob.NewDecoder(&b) + // d := gob.NewDecoder(bytes.NewBufferString(str)) + err = d.Decode(&m) + if err != nil { + return err + } + return nil +} diff --git a/adaptors/sessions/database.go b/adaptors/sessions/database.go new file mode 100644 index 00000000..e74f08c4 --- /dev/null +++ b/adaptors/sessions/database.go @@ -0,0 +1,17 @@ +package sessions + +// Database is the interface which all session databases should implement +// By design it doesn't support any type of cookie session like other frameworks, +// I want to protect you, believe me, no context access (although we could) +// The scope of the database is to session somewhere the sessions in order to +// keep them after restarting the server, nothing more. +// the values are sessiond by the underline session, the check for new sessions, or +// 'this session value should added' are made automatically by q, 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 Database interface { + Load(string) map[string]interface{} + Update(string, map[string]interface{}) +} diff --git a/adaptors/sessions/provider.go b/adaptors/sessions/provider.go new file mode 100644 index 00000000..e0dee02f --- /dev/null +++ b/adaptors/sessions/provider.go @@ -0,0 +1,126 @@ +package sessions + +import ( + "sync" + "time" + + "gopkg.in/kataras/iris.v6" +) + +type ( + // provider contains the sessions and external databases (load and update). + // It's the session memory manager + provider struct { + // we don't use RWMutex because all actions have read and write at the same action function. + // (or write to a *session's value which is race if we don't lock) + // narrow locks are fasters but are useless here. + mu sync.Mutex + sessions map[string]*session + databases []Database + } +) + +// newProvider returns a new sessions provider +func newProvider() *provider { + return &provider{ + sessions: make(map[string]*session, 0), + databases: make([]Database, 0), + } +} + +// RegisterDatabase adds a session database +// a session db doesn't have write access +func (p *provider) RegisterDatabase(db Database) { + p.mu.Lock() // for any case + p.databases = append(p.databases, db) + p.mu.Unlock() +} + +// newSession returns a new session from sessionid +func (p *provider) newSession(sid string, expires time.Duration) *session { + + sess := &session{ + sid: sid, + provider: p, + values: p.loadSessionValues(sid), + flashes: make(map[string]*flashMessage), + } + + if expires > 0 { // if not unlimited life duration and no -1 (cookie remove action is based on browser's session) + time.AfterFunc(expires, func() { + // the destroy makes the check if this session is exists then or not, + // this is used to destroy the session from the server-side also + // it's good to have here for security reasons, I didn't add it on the gc function to separate its action + p.Destroy(sid) + }) + } + + return sess +} + +func (p *provider) 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 *provider) 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 *provider) Init(sid string, expires time.Duration) iris.Session { + newSession := p.newSession(sid, expires) + p.mu.Lock() + p.sessions[sid] = newSession + p.mu.Unlock() + return newSession +} + +// Read returns the store which sid parameter belongs +func (p *provider) Read(sid string, expires time.Duration) iris.Session { + p.mu.Lock() + if sess, found := p.sessions[sid]; found { + sess.runFlashGC() // run the flash messages GC, new request here of existing session + p.mu.Unlock() + return sess + } + p.mu.Unlock() + return p.Init(sid, expires) // if not found create new + +} + +// Destroy destroys the session, removes all sessions and flash values, +// the session itself and updates the registered session databases, +// this called from sessionManager which removes the client's cookie also. +func (p *provider) Destroy(sid string) { + p.mu.Lock() + if sess, found := p.sessions[sid]; found { + sess.values = nil + sess.flashes = nil + delete(p.sessions, sid) + p.updateDatabases(sid, nil) + } + p.mu.Unlock() + +} + +// DestroyAll removes all sessions +// from the server-side memory (and database if registered). +// Client's session cookie will still exist but it will be reseted on the next request. +func (p *provider) DestroyAll() { + p.mu.Lock() + for _, sess := range p.sessions { + delete(p.sessions, sess.ID()) + p.updateDatabases(sess.ID(), nil) + } + p.mu.Unlock() + +} diff --git a/adaptors/sessions/session.go b/adaptors/sessions/session.go new file mode 100644 index 00000000..6a784ffc --- /dev/null +++ b/adaptors/sessions/session.go @@ -0,0 +1,294 @@ +package sessions + +import ( + "strconv" + "sync" + "time" + + "github.com/kataras/go-errors" + "gopkg.in/kataras/iris.v6" +) + +type ( + + // session is an 'object' which wraps the session provider with its session databases, only frontend user has access to this session object. + // implements the iris.Session interface + session struct { + sid string + values map[string]interface{} // here are the real values + // we could set the flash messages inside values but this will bring us more problems + // because of session databases and because of + // users may want to get all sessions and save them or display them + // but without temp values (flash messages) which are removed after fetching. + // so introduce a new field here. + // NOTE: flashes are not managed by third-party, only inside session struct. + flashes map[string]*flashMessage + mu sync.RWMutex + createdAt time.Time + provider *provider + } + + flashMessage struct { + // if true then this flash message is removed on the flash gc + shouldRemove bool + value interface{} + } +) + +var _ iris.Session = &session{} + +// 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.mu.RLock() + value := s.values[key] + s.mu.RUnlock() + + return value +} + +// when running on the session manager removes any 'old' flash messages +func (s *session) runFlashGC() { + s.mu.Lock() + for key, v := range s.flashes { + if v.shouldRemove { + delete(s.flashes, key) + } + } + s.mu.Unlock() +} + +// HasFlash returns true if this request has available flash messages +func (s *session) HasFlash() bool { + return s.flashes != nil && len(s.flashes) > 0 +} + +// GetFlash returns a flash message which removed on the next request +// +// To check for flash messages we use the HasFlash() Method +// and to obtain the flash message we use the GetFlash() Method. +// There is also a method GetFlashes() to fetch all the messages. +// +// Fetching a message deletes it from the session. +// This means that a message is meant to be displayed only on the first page served to the user +func (s *session) GetFlash(key string) (v interface{}) { + s.mu.Lock() + if valueStorage, found := s.flashes[key]; found { + valueStorage.shouldRemove = true + v = valueStorage.value + } + s.mu.Unlock() + + return +} + +// 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 "" +} + +// GetFlashString same as GetFlash but returns as string, if nil then returns an empty string +func (s *session) GetFlashString(key string) string { + if value := s.GetFlash(key); value != nil { + if v, ok := value.(string); ok { + return v + } + } + + return "" +} + +var errFindParse = errors.New("Unable to find the %s with key: %s. Found? %#v") + +// GetInt same as Get but returns as int, if not found then returns -1 and an error +func (s *session) GetInt(key string) (int, error) { + v := s.Get(key) + if vint, ok := v.(int); ok { + return vint, nil + } else if vstring, sok := v.(string); sok { + return strconv.Atoi(vstring) + } + + return -1, errFindParse.Format("int", key, v) +} + +// GetInt64 same as Get but returns as int64, if not found then returns -1 and an error +func (s *session) GetInt64(key string) (int64, error) { + v := s.Get(key) + if vint64, ok := v.(int64); ok { + return vint64, nil + } else if vint, ok := v.(int); ok { + return int64(vint), nil + } else if vstring, sok := v.(string); sok { + return strconv.ParseInt(vstring, 10, 64) + } + + return -1, errFindParse.Format("int64", key, v) + +} + +// GetFloat32 same as Get but returns as float32, if not found then returns -1 and an error +func (s *session) GetFloat32(key string) (float32, error) { + v := s.Get(key) + if vfloat32, ok := v.(float32); ok { + return vfloat32, nil + } else if vfloat64, ok := v.(float64); ok { + return float32(vfloat64), nil + } else if vint, ok := v.(int); ok { + return float32(vint), nil + } else if vstring, sok := v.(string); sok { + vfloat64, err := strconv.ParseFloat(vstring, 32) + if err != nil { + return -1, err + } + return float32(vfloat64), nil + } + + return -1, errFindParse.Format("float32", key, v) +} + +// GetFloat64 same as Get but returns as float64, if not found then returns -1 and an error +func (s *session) GetFloat64(key string) (float64, error) { + v := s.Get(key) + if vfloat32, ok := v.(float32); ok { + return float64(vfloat32), nil + } else if vfloat64, ok := v.(float64); ok { + return vfloat64, nil + } else if vint, ok := v.(int); ok { + return float64(vint), nil + } else if vstring, sok := v.(string); sok { + return strconv.ParseFloat(vstring, 32) + } + + return -1, errFindParse.Format("float64", key, v) +} + +// GetBoolean same as Get but returns as boolean, if not found then returns -1 and an error +func (s *session) GetBoolean(key string) (bool, error) { + v := s.Get(key) + // here we could check for "true", "false" and 0 for false and 1 for true + // but this may cause unexpected behavior from the developer if they expecting an error + // so we just check if bool, if yes then return that bool, otherwise return false and an error + if vb, ok := v.(bool); ok { + return vb, nil + } + + return false, errFindParse.Format("bool", key, v) +} + +// GetAll returns a copy of all session's values +func (s *session) GetAll() map[string]interface{} { + items := make(map[string]interface{}, len(s.values)) + s.mu.RLock() + for key, v := range s.values { + items[key] = v + } + s.mu.RUnlock() + return items +} + +// GetFlashes returns all flash messages as map[string](key) and interface{} value +// NOTE: this will cause at remove all current flash messages on the next request of the same user +func (s *session) GetFlashes() map[string]interface{} { + flashes := make(map[string]interface{}, len(s.flashes)) + s.mu.Lock() + for key, v := range s.flashes { + flashes[key] = v.value + v.shouldRemove = true + } + s.mu.Unlock() + return flashes +} + +// 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.updateDatabases() +} + +// SetFlash sets a flash message by its key. +// +// A flash message is used in order to keep a message in session through one or several requests of the same user. +// It is removed from session after it has been displayed to the user. +// Flash messages are usually used in combination with HTTP redirections, +// because in this case there is no view, so messages can only be displayed in the request that follows redirection. +// +// A flash message has a name and a content (AKA key and value). +// It is an entry of an associative array. The name is a string: often "notice", "success", or "error", but it can be anything. +// The content is usually a string. You can put HTML tags in your message if you display it raw. +// You can also set the message value to a number or an array: it will be serialized and kept in session like a string. +// +// Flash messages can be set using the SetFlash() Method +// For example, if you would like to inform the user that his changes were successfully saved, +// you could add the following line to your Handler: +// +// SetFlash("success", "Data saved!"); +// +// In this example we used the key 'success'. +// If you want to define more than one flash messages, you will have to use different keys +func (s *session) SetFlash(key string, value interface{}) { + s.mu.Lock() + s.flashes[key] = &flashMessage{value: value} + s.mu.Unlock() +} + +// Delete removes an entry by its key +func (s *session) Delete(key string) { + s.mu.Lock() + delete(s.values, key) + s.mu.Unlock() + + s.updateDatabases() +} + +func (s *session) updateDatabases() { + s.provider.updateDatabases(s.sid, s.values) +} + +// DeleteFlash removes a flash message by its key +func (s *session) DeleteFlash(key string) { + s.mu.Lock() + delete(s.flashes, key) + s.mu.Unlock() +} + +// Clear removes all entries +func (s *session) Clear() { + s.mu.Lock() + for key := range s.values { + delete(s.values, key) + } + s.mu.Unlock() + + s.updateDatabases() +} + +// Clear removes all flash messages +func (s *session) ClearFlashes() { + s.mu.Lock() + for key := range s.flashes { + delete(s.flashes, key) + } + s.mu.Unlock() +} diff --git a/adaptors/sessions/sessiondb/README.md b/adaptors/sessions/sessiondb/README.md new file mode 100644 index 00000000..0180e544 --- /dev/null +++ b/adaptors/sessions/sessiondb/README.md @@ -0,0 +1,29 @@ +## Session databases + +Find more databases at [github.com/kataras/go-sessions/sessiondb](https://github.com/kataras/go-sessions/tree/master/sessiondb). + +This folder contains only the redis database because the rest (two so far, 'file' and 'leveldb') were created by the Community. +So go [there](https://github.com/kataras/go-sessions/tree/master/sessiondb) and find more about them. `Database` is just an +interface so you're able to `UseDatabase(anyCompatibleDatabase)`. A Database should implement two functions, `Load` and `Update`. + +**Database interface** + +```go +type Database interface { + Load(string) map[string]interface{} + Update(string, map[string]interface{}) +} +``` + +```go +import ( + "...myDatabase" +) +s := New(...) +s.UseDatabase(myDatabase) // <--- + +app := iris.New() +app.Adapt(s) + +app.Listen(":8080") +``` diff --git a/adaptors/sessions/sessiondb/redis/database.go b/adaptors/sessions/sessiondb/redis/database.go new file mode 100644 index 00000000..01f90433 --- /dev/null +++ b/adaptors/sessions/sessiondb/redis/database.go @@ -0,0 +1,85 @@ +package redis + +import ( + "bytes" + "encoding/gob" + + "gopkg.in/kataras/iris.v6/adaptors/sessions/sessiondb/redis/service" +) + +// Database the redis database for q sessions +type Database struct { + redis *service.Service +} + +// New returns a new redis database +func New(cfg ...service.Config) *Database { + return &Database{redis: service.New(cfg...)} +} + +// Config returns the configuration for the redis server bridge, you can change them +func (d *Database) Config() *service.Config { + return d.redis.Config +} + +// Load loads the values to the underline +func (d *Database) Load(sid string) map[string]interface{} { + values := make(map[string]interface{}) + + if !d.redis.Connected { //yes, check every first time's session for valid redis connection + d.redis.Connect() + _, err := d.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 Connect: " + err.Error()) + println("But don't panic, auto-switching to memory store right now!") + } + } + } + //fetch the values from this session id and copy-> store them + val, err := d.redis.GetBytes(sid) + if err == nil { + err = DeserializeBytes(val, &values) + } + + return values + +} + +// serialize the values to be stored as strings inside the Redis, we panic at any serialization error here +func serialize(values map[string]interface{}) []byte { + val, err := SerializeBytes(values) + if err != nil { + println("On redisstore.serialize: " + err.Error()) + } + + return val +} + +// Update updates the real redis store +func (d *Database) Update(sid string, newValues map[string]interface{}) { + if len(newValues) == 0 { + go d.redis.Delete(sid) + } else { + go d.redis.Set(sid, serialize(newValues)) //set/update all the values + } + +} + +// SerializeBytes serializa bytes using gob encoder and returns them +func SerializeBytes(m interface{}) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + err := enc.Encode(m) + if err == nil { + return buf.Bytes(), nil + } + return nil, err +} + +// DeserializeBytes converts the bytes to an object using gob decoder +func DeserializeBytes(b []byte, m interface{}) error { + dec := gob.NewDecoder(bytes.NewBuffer(b)) + return dec.Decode(m) //no reference here otherwise doesn't work because of go remote object +} diff --git a/adaptors/sessions/sessiondb/redis/service/config.go b/adaptors/sessions/sessiondb/redis/service/config.go new file mode 100644 index 00000000..c5b600ba --- /dev/null +++ b/adaptors/sessions/sessiondb/redis/service/config.go @@ -0,0 +1,78 @@ +package service + +import ( + "time" + + "github.com/imdario/mergo" +) + +const ( + // 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 +) + +// Config the redis configuration used inside sessions +type Config 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 +} + +// DefaultConfig returns the default configuration for Redis service +func DefaultConfig() Config { + return Config{ + 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 Config) Merge(cfg []Config) (config Config) { + + 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 Config) MergeSingle(cfg Config) (config Config) { + + config = cfg + mergo.Merge(&config, c) + + return +} diff --git a/adaptors/sessions/sessiondb/redis/service/service.go b/adaptors/sessions/sessiondb/redis/service/service.go new file mode 100644 index 00000000..ff729ece --- /dev/null +++ b/adaptors/sessions/sessiondb/redis/service/service.go @@ -0,0 +1,272 @@ +package service + +import ( + "time" + + "github.com/garyburd/redigo/redis" + "github.com/kataras/go-errors" +) + +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 + 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 +} + +// 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) (err error) { // map[interface{}]interface{}) (err error) { + c := r.pool.Get() + defer c.Close() + if err = c.Err(); err != nil { + return + } + _, err = c.Do("SETEX", r.Config.Prefix+key, r.Config.MaxAgeSeconds, 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 = DefaultRedisNetwork + } + if addr == "" { + addr = 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 = DefaultRedisIdleTimeout + } + + if c.Network == "" { + c.Network = DefaultRedisNetwork + } + + if c.Addr == "" { + c.Addr = DefaultRedisAddr + } + + if c.MaxAgeSeconds <= 0 { + c.MaxAgeSeconds = DefaultRedisMaxAgeSeconds + } + + pool := &redis.Pool{IdleTimeout: 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) *Service { + c := DefaultConfig().Merge(cfg) + r := &Service{pool: &redis.Pool{}, Config: &c} + return r +} diff --git a/adaptors/sessions/sessions.go b/adaptors/sessions/sessions.go new file mode 100644 index 00000000..085d8d2c --- /dev/null +++ b/adaptors/sessions/sessions.go @@ -0,0 +1,188 @@ +// Package sessions as originally written by me at https://github.com/kataras/go-sessions +// Based on kataras/go-sessions v1.0.0. +// +// Edited for Iris v6 (or iris vNext) and removed all fasthttp things in order to reduce the +// compiled and go getable size. The 'file' and 'leveldb' databases are missing +// because they written by community, not me, you can still adapt any database with +// .UseDatabase because it expects an interface, +// find more databases here: https://github.com/kataras/go-sessions/tree/master/sessiondb +package sessions + +import ( + "encoding/base64" + "net/http" + "strings" + "time" + + "gopkg.in/kataras/iris.v6" +) + +type ( + // Sessions is the start point of this package + // contains all the registered sessions and manages them + Sessions interface { + // Adapt is used to adapt this sessions manager as an iris.SessionsPolicy + // to an Iris station. + // It's being used by the framework, developers should not actually call this function. + Adapt(*iris.Policies) + + // UseDatabase ,optionally, adds a session database to the manager's provider, + // a session db doesn't have write access + // see https://github.com/kataras/go-sessions/tree/master/sessiondb + UseDatabase(Database) + + // Start starts the session for the particular net/http request + Start(http.ResponseWriter, *http.Request) iris.Session + + // Destroy kills the net/http session and remove the associated cookie + Destroy(http.ResponseWriter, *http.Request) + + // DestroyByID removes the session entry + // from the server-side memory (and database if registered). + // Client's session cookie will still exist but it will be reseted on the next request. + // + // It's safe to use it even if you are not sure if a session with that id exists. + DestroyByID(string) + // DestroyAll removes all sessions + // from the server-side memory (and database if registered). + // Client's session cookie will still exist but it will be reseted on the next request. + DestroyAll() + } + + // sessions contains the cookie's name, the provider and a duration for GC and cookie life expire + sessions struct { + config Config + provider *provider + } +) + +// New returns a new fast, feature-rich sessions manager +// it can be adapted to an Iris station +func New(cfg Config) Sessions { + return &sessions{ + config: cfg.Validate(), + provider: newProvider(), + } +} + +func (s *sessions) Adapt(frame *iris.Policies) { + // for newcomers this maybe looks strange: + // Each policy is an adaptor too, so they all can contain an Adapt. + // If they contains an Adapt func then the policy is an adaptor too and this Adapt func is called + // by Iris on .Adapt(...) + policy := iris.SessionsPolicy{ + Start: s.Start, + Destroy: s.Destroy, + } + + policy.Adapt(frame) +} + +// UseDatabase adds a session database to the manager's provider, +// a session db doesn't have write access +func (s *sessions) UseDatabase(db Database) { + s.provider.RegisterDatabase(db) +} + +// Start starts the session for the particular net/http request +func (s *sessions) Start(res http.ResponseWriter, req *http.Request) iris.Session { + var sess iris.Session + + cookieValue := GetCookie(s.config.Cookie, req) + if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie + sid := SessionIDGenerator(s.config.CookieLength) + sess = s.provider.Init(sid, s.config.Expires) + cookie := &http.Cookie{} + + // The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding + cookie.Name = s.config.Cookie + cookie.Value = sid + cookie.Path = "/" + if !s.config.DisableSubdomainPersistence { + + requestDomain := req.URL.Host + if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 { + requestDomain = requestDomain[0:portIdx] + } + if IsValidCookieDomain(requestDomain) { + + // RFC2109, we allow level 1 subdomains, but no further + // if we have localhost.com , we want the localhost.cos. + // 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.Domain = "." + requestDomain // . to allow persistance + } + + } + cookie.HttpOnly = true + // MaxAge=0 means no 'Max-Age' attribute specified. + // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' + // MaxAge>0 means Max-Age attribute present and given in seconds + if s.config.Expires >= 0 { + if s.config.Expires == 0 { // unlimited life + cookie.Expires = CookieExpireUnlimited + } else { // > 0 + cookie.Expires = time.Now().Add(s.config.Expires) + } + cookie.MaxAge = int(cookie.Expires.Sub(time.Now()).Seconds()) + } else { + // if it's -1 then the cookie is deleted when the browser closes + // so MaxAge = -1 + cookie.MaxAge = -1 + } + + AddCookie(cookie, res) + } else { + sess = s.provider.Read(cookieValue, s.config.Expires) + } + return sess +} + +// Destroy kills the net/http session and remove the associated cookie +func (s *sessions) Destroy(res http.ResponseWriter, req *http.Request) { + cookieValue := GetCookie(s.config.Cookie, req) + if cookieValue == "" { // nothing to destroy + return + } + RemoveCookie(s.config.Cookie, res, req) + s.provider.Destroy(cookieValue) +} + +// DestroyByID removes the session entry +// from the server-side memory (and database if registered). +// Client's session cookie will still exist but it will be reseted on the next request. +// +// It's safe to use it even if you are not sure if a session with that id exists. +// Works for both net/http +func (s *sessions) DestroyByID(sid string) { + s.provider.Destroy(sid) +} + +// DestroyAll removes all sessions +// from the server-side memory (and database if registered). +// Client's session cookie will still exist but it will be reseted on the next request. +// Works for both net/http +func (s *sessions) DestroyAll() { + s.provider.DestroyAll() +} + +// SessionIDGenerator returns a random string, used to set the session id +// you are able to override this to use your own method for generate session ids +var SessionIDGenerator = func(strLength int) string { + return base64.URLEncoding.EncodeToString(random(strLength)) +} diff --git a/adaptors/typescript/typescript.go b/adaptors/typescript/typescript.go index 035e36c2..57a1fb78 100644 --- a/adaptors/typescript/typescript.go +++ b/adaptors/typescript/typescript.go @@ -1,3 +1,8 @@ +// Package typescript provides a typescript compiler with hot-reloader +// and optionally a cloud-based editor, called 'alm-tools'. +// typescript (by microsoft) and alm-tools (by basarat) have their own (open-source) licenses +// the tools are not used directly by this adaptor, but it's good to know where you can find +// the software. package typescript import ( diff --git a/adaptors/view/adaptor.go b/adaptors/view/adaptor.go index 6c11a0e4..feeee04f 100644 --- a/adaptors/view/adaptor.go +++ b/adaptors/view/adaptor.go @@ -1,3 +1,5 @@ +// Package view is the adaptor of the 5 template engines +// as written by me at https://github.com/kataras/go-template package view import ( diff --git a/adaptors/websocket/websocket.go b/adaptors/websocket/websocket.go index 9e92ef54..0578b790 100644 --- a/adaptors/websocket/websocket.go +++ b/adaptors/websocket/websocket.go @@ -1,4 +1,5 @@ // Package websocket provides an easy way to setup server and client side rich websocket experience for Iris +// As originally written by me at https://github.com/kataras/go-websocket package websocket import ( diff --git a/configuration.go b/configuration.go index a7005d97..ef389fb3 100644 --- a/configuration.go +++ b/configuration.go @@ -9,7 +9,6 @@ import ( "github.com/imdario/mergo" "github.com/kataras/go-options" - "github.com/kataras/go-sessions" ) type ( @@ -174,9 +173,6 @@ type Configuration struct { // Defaults to false Gzip bool - // Sessions contains the configs for sessions - Sessions SessionsConfiguration - // Other are the custom, dynamic options, can be empty // this fill used only by you to set any app's options you want // for each of an Iris instance @@ -439,92 +435,10 @@ func DefaultConfiguration() Configuration { TimeFormat: DefaultTimeFormat, Charset: DefaultCharset, Gzip: false, - Sessions: DefaultSessionsConfiguration(), Other: options.Options{}, } } -// SessionsConfiguration the configuration for sessions -// has 6 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 cookie length (sessionid) int, Defaults to 32, do not change if you don't have any reason to do -// fifth is the gcDuration (time.Duration) when this time passes it removes the unused sessions from the memory until the user come back -// sixth is the DisableSubdomainPersistence which you can set it to true in order dissallow your q subdomains to have access to the session cook -type SessionsConfiguration sessions.Config - -// Set implements the OptionSetter of the sessions package -func (s SessionsConfiguration) Set(c *sessions.Config) { - *c = sessions.Config(s).Validate() -} - -var ( - // OptionSessionsCookie string, the session's client cookie name, for example: "qsessionid" - OptionSessionsCookie = func(val string) OptionSet { - return func(c *Configuration) { - c.Sessions.Cookie = val - } - } - - // OptionSessionsDecodeCookie set it to true to decode the cookie key with base64 URLEncoding - // Defaults to false - OptionSessionsDecodeCookie = func(val bool) OptionSet { - return func(c *Configuration) { - c.Sessions.DecodeCookie = val - } - } - - // OptionSessionsExpires the duration of which the cookie must expires (created_time.Add(Expires)). - // If you want to delete the cookie when the browser closes, set it to -1 but in this case, the server side's session duration is up to GcDuration - // - // Default infinitive/unlimited life duration(0) - OptionSessionsExpires = func(val time.Duration) OptionSet { - return func(c *Configuration) { - c.Sessions.Expires = val - } - } - - // OptionSessionsCookieLength the length of the sessionid's cookie's value, let it to 0 if you don't want to change it - // Defaults to 32 - OptionSessionsCookieLength = func(val int) OptionSet { - return func(c *Configuration) { - c.Sessions.CookieLength = val - } - } - - // OptionSessionsDisableSubdomainPersistence set it to true in order dissallow your q subdomains to have access to the session cookie - // Defaults to false - OptionSessionsDisableSubdomainPersistence = func(val bool) OptionSet { - return func(c *Configuration) { - c.Sessions.DisableSubdomainPersistence = val - } - } -) - -var ( - // CookieExpireNever the default cookie's life for sessions, unlimited (23 years) - CookieExpireNever = time.Now().AddDate(23, 0, 0) -) - -const ( - // DefaultCookieName the secret cookie's name for sessions - DefaultCookieName = "irissessionid" - // DefaultCookieLength is the default Session Manager's CookieLength, which is 32 - DefaultCookieLength = 32 -) - -// DefaultSessionsConfiguration the default configs for Sessions -func DefaultSessionsConfiguration() SessionsConfiguration { - return SessionsConfiguration{ - Cookie: DefaultCookieName, - CookieLength: DefaultCookieLength, - DecodeCookie: false, - Expires: 0, - DisableSubdomainPersistence: false, - } -} - // Default values for base Server conf const ( // DefaultServerHostname returns the default hostname which is 0.0.0.0 diff --git a/context.go b/context.go index 37e45dcf..99e3e97d 100644 --- a/context.go +++ b/context.go @@ -23,7 +23,6 @@ import ( "github.com/iris-contrib/formBinder" "github.com/kataras/go-errors" "github.com/kataras/go-fs" - "github.com/kataras/go-sessions" "github.com/kataras/go-template" ) @@ -215,7 +214,7 @@ type ( framework *Framework //keep track all registered middleware (handlers) Middleware Middleware // exported because is useful for debugging - session sessions.Session + session Session // Pos is the position number of the Context, look .Next to understand Pos int // exported because is useful for debugging } @@ -1484,22 +1483,82 @@ func (ctx *Context) RemoveCookie(name string) { ctx.Request.Header.Set("Cookie", "") } -// Session returns the current session ( && flash messages ) -func (ctx *Context) Session() sessions.Session { - if ctx.framework.sessions == nil { // this should never return nil but FOR ANY CASE, on future changes. +var errSessionsPolicyIsMissing = errors.New( + ` +manually call of context.Session() for client IP: '%s' without specified SessionsPolicy! +Please .Adapt one of the available session managers inside 'kataras/iris/adaptors'. + +Edit your main .go source file to adapt one of these and restart your app. + i.e: lines (<---) were missing. + ------------------------------------------------------------------- + import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/httprouter" // or gorillamux + "github.com/kataras/iris/adaptors/sessions" // <--- this line + ) + + func main(){ + app := iris.New() + // right below the iris.New() + app.Adapt(httprouter.New()) // or gorillamux.New() + + mySessions := sessions.New(sessions.Config{ + // Cookie string, the session's client cookie name, for example: "mysessionid" + // + // Defaults to "gosessionid" + Cookie: "mysessionid", + // base64 urlencoding, + // if you have strange name cookie name enable this + DecodeCookie: false, + // it's time.Duration, from the time cookie is created, how long it can be alive? + // 0 means no expire. + Expires: 0, + // the length of the sessionid's cookie's value + CookieLength: 32, + // if you want to invalid cookies on different subdomains + // of the same host, then enable it + DisableSubdomainPersistence: false, + }) + + // OPTIONALLY: + // import "gopkg.in/kataras/iris.v6/adaptors/sessions/sessiondb/redis" + // or import "github.com/kataras/go-sessions/sessiondb/$any_available_community_database" + // mySessions.UseDatabase(redis.New(...)) + + app.Adapt(mySessions) // <--- and this line were missing. + + // the rest of your source code... + // ... + + app.Listen("%s") + } + ------------------------------------------------------------------- + `) + +// Session returns the current Session. +// +// if SessionsPolicy is missing then a detailed how-to-fix message +// will be visible to the user (DevMode) +// and the return value will be NILL. +func (ctx *Context) Session() Session { + policy := ctx.framework.policies.SessionsPolicy + if policy.Start == nil { + ctx.framework.Log(DevMode, + errSessionsPolicyIsMissing.Format(ctx.RemoteAddr(), ctx.framework.Config.VHost).Error()) return nil } if ctx.session == nil { - ctx.session = ctx.framework.sessions.Start(ctx.ResponseWriter, ctx.Request) + ctx.session = policy.Start(ctx.ResponseWriter, ctx.Request) } + return ctx.session } // SessionDestroy destroys the whole session, calls the provider's destroy and remove the cookie func (ctx *Context) SessionDestroy() { if sess := ctx.Session(); sess != nil { - ctx.framework.sessions.Destroy(ctx.ResponseWriter, ctx.Request) + ctx.framework.policies.SessionsPolicy.Destroy(ctx.ResponseWriter, ctx.Request) } } diff --git a/iris.go b/iris.go index 0055d552..e9c781bb 100644 --- a/iris.go +++ b/iris.go @@ -26,7 +26,6 @@ import ( "github.com/kataras/go-errors" "github.com/kataras/go-fs" "github.com/kataras/go-serializer" - "github.com/kataras/go-sessions" ) const ( @@ -70,9 +69,8 @@ type Framework struct { ln net.Listener closedManually bool - once sync.Once - Config *Configuration - sessions sessions.Sessions + once sync.Once + Config *Configuration } var defaultGlobalLoggerOuput = log.New(os.Stdout, "[iris] ", log.LstdFlags) @@ -236,22 +234,6 @@ func New(setters ...OptionSetter) *Framework { } - { - // +------------------------------------------------------------+ - // | Module Name: Sessions | - // | On Init: Attach a session manager with empty config | - // | On Build: Set the configuration if allowed | - // +------------------------------------------------------------+ - - // set the sessions in order to UseSessionDB to work - s.sessions = sessions.New() - // On Build: - s.Adapt(EventPolicy{Build: func(*Framework) { - // re-set the configuration field to update users configuration - s.sessions.Set(s.Config.Sessions) - }}) - } - { // +------------------------------------------------------------+ // | Module Name: Router | @@ -645,32 +627,6 @@ func (s *Framework) Adapt(policies ...Policy) { } } -// 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 sessions.Database) { - s.sessions.UseDatabase(db) -} - -// DestroySessionByID removes the session entry -// from the server-side memory (and database if registered). -// Client's session cookie will still exist but it will be reseted on the next request. -// -// It's safe to use it even if you are not sure if a session with that id exists. -func (s *Framework) DestroySessionByID(sid string) { - s.sessions.DestroyByID(sid) -} - -// DestroyAllSessions removes all sessions -// from the server-side memory (and database if registered). -// Client's session cookie will still exist but it will be reseted on the next request. -func (s *Framework) DestroyAllSessions() { - s.sessions.DestroyAll() -} - // cachedMuxEntry is just a wrapper for the Cache functionality // it seems useless but I prefer to keep the cached handler on its own memory stack, // reason: no clojures hell in the Cache function @@ -831,10 +787,14 @@ Edit your main .go source file to adapt one of these and restart your app. func main(){ app := iris.New() + // right below the iris.New(): app.Adapt(httprouter.New()) // or gorillamux.New() - // right below the iris.New() + app.Adapt(view.HTML("./templates", ".html")) // <--- and this line were missing. + // the rest of your source code... + // ... + app.Listen("%s") } ------------------------------------------------------------------- diff --git a/policy.go b/policy.go index af617a73..3f266a03 100644 --- a/policy.go +++ b/policy.go @@ -31,6 +31,7 @@ type ( RouterWrapperPolicy RenderPolicy TemplateFuncsPolicy + SessionsPolicy } ) @@ -71,6 +72,8 @@ func (p Policies) Adapt(frame *Policies) { p.TemplateFuncsPolicy.Adapt(frame) } + p.SessionsPolicy.Adapt(frame) + } // LogMode is the type for the LoggerPolicy write mode. @@ -430,3 +433,95 @@ func (t TemplateFuncsPolicy) Adapt(frame *Policies) { } } } + +type ( + // Author's notes: + // session manager can work as a middleware too + // but we want an easy-api for the user + // as we did before with: context.Session().Set/Get... + // these things cannot be done with middleware and sessions is a critical part of an application + // which needs attention, so far we used the kataras/go-sessions which I spent many weeks to create + // and that time has not any known bugs or any other issues, it's fully featured. + // BUT user may want to use other session library and in the same time users should be able to use + // iris' api for sessions from context, so a policy is that we need, the policy will contains + // the Start(responsewriter, request) and the Destroy(responsewriter, request) + // (keep note that this Destroy is not called at the end of a handler, Start does its job without need to end something + // sessions are setting in real time, when the user calls .Set ), + // the Start(responsewriter, request) will return a 'Session' which will contain the API for context.Session() , it should be + // rich, as before, so the interface will be a clone of the kataras/go-sessions/Session. + // If the user wants to use other library and that library missing features that kataras/go-sesisons has + // then the user should make an empty implementation of these calls in order to work. + // That's no problem, before they couldn't adapt any session manager, now they will can. + // + // The databases or stores registration will be in the session manager's responsibility, + // as well the DestroyByID and DestroyAll (I'm calling these with these names because + // I take as base the kataras/go-sessions, + // I have no idea if other session managers + // supports these things, if not then no problem, + // these funcs will be not required by the sessions policy) + // + // ok let's begin. + + // Session should expose the SessionsPolicy's end-user API. + // This will be returned at the sess := context.Session(). + Session interface { + ID() string + Get(string) interface{} + HasFlash() bool + GetFlash(string) interface{} + GetString(key string) string + GetFlashString(string) string + GetInt(key string) (int, error) + GetInt64(key string) (int64, error) + GetFloat32(key string) (float32, error) + GetFloat64(key string) (float64, error) + GetBoolean(key string) (bool, error) + GetAll() map[string]interface{} + GetFlashes() map[string]interface{} + VisitAll(cb func(k string, v interface{})) + Set(string, interface{}) + SetFlash(string, interface{}) + Delete(string) + DeleteFlash(string) + Clear() + ClearFlashes() + } + + // SessionsPolicy is the policy for a session manager. + // + // A SessionsPolicy should be responsible to Start a sesion based + // on raw http.ResponseWriter and http.Request, which should return + // a compatible iris.Session interface, type. If the external session manager + // doesn't qualifies, then the user should code the rest of the functions with empty implementation. + // + // A SessionsPolicy should be responsible to Destory a session based + // on the http.ResponseWriter and http.Request, this function should works individually. + // + // No iris.Context required from users. In order to be able to adapt any external session manager. + // + // The SessionsPolicy should be adapted once. + SessionsPolicy struct { + // Start should starts the session for the particular net/http request + Start func(http.ResponseWriter, *http.Request) Session + + // Destroy should kills the net/http session and remove the associated cookie + // Keep note that: Destroy should not called at the end of any handler, it's an independent func. + // Start should set + // the values at realtime and if manager doesn't supports these + // then the user manually have to call its 'done' func inside the handler. + Destroy func(http.ResponseWriter, *http.Request) + } +) + +// Adapt adaps a SessionsPolicy object to the main *Policies. +// +// Remember: Each policy is an adaptor. +// An adaptor should contains one or more policies too. +func (s SessionsPolicy) Adapt(frame *Policies) { + if s.Start != nil { + frame.SessionsPolicy.Start = s.Start + } + if s.Destroy != nil { + frame.SessionsPolicy.Destroy = s.Destroy + } +} diff --git a/router.go b/router.go index fe42ab4d..52971347 100644 --- a/router.go +++ b/router.go @@ -80,6 +80,9 @@ Edit your main .go source file to adapt one of these routers and restart your ap // right below the iris.New() app.Adapt(httprouter.New()) // <--- and this line were missing. + // the rest of your source code... + // ... + app.Listen("%s") }