mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
SessionsPolicy and sessions adaptor, history and _example written.
Former-commit-id: e8b0dde3cb3b72919f01b9d836d8ccb3d4e20214
This commit is contained in:
parent
82afcc5aa6
commit
13e83fc57e
115
HISTORY.md
115
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
|
||||
|
||||
|
|
21
adaptors/sessions/LICENSE
Normal file
21
adaptors/sessions/LICENSE
Normal file
|
@ -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.
|
87
adaptors/sessions/_example/main.go
Normal file
87
adaptors/sessions/_example/main.go
Normal file
|
@ -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")
|
||||
}
|
74
adaptors/sessions/config.go
Normal file
74
adaptors/sessions/config.go
Normal file
|
@ -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
|
||||
}
|
165
adaptors/sessions/cookie.go
Normal file
165
adaptors/sessions/cookie.go
Normal file
|
@ -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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
// random takes a parameter (int) and returns random slice of byte
|
||||
// ex: var randomstrbytes []byte; randomstrbytes = Random(32)
|
||||
// note: this code doesn't belongs to me, but it works just fine*
|
||||
//
|
||||
// Used for the default SessionIDGenerator which you can change.
|
||||
func random(n int) []byte {
|
||||
src := rand.NewSource(time.Now().UnixNano())
|
||||
b := make([]byte, n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 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
|
||||
}
|
17
adaptors/sessions/database.go
Normal file
17
adaptors/sessions/database.go
Normal file
|
@ -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{})
|
||||
}
|
126
adaptors/sessions/provider.go
Normal file
126
adaptors/sessions/provider.go
Normal file
|
@ -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()
|
||||
|
||||
}
|
294
adaptors/sessions/session.go
Normal file
294
adaptors/sessions/session.go
Normal file
|
@ -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()
|
||||
}
|
29
adaptors/sessions/sessiondb/README.md
Normal file
29
adaptors/sessions/sessiondb/README.md
Normal file
|
@ -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")
|
||||
```
|
85
adaptors/sessions/sessiondb/redis/database.go
Normal file
85
adaptors/sessions/sessiondb/redis/database.go
Normal file
|
@ -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
|
||||
}
|
78
adaptors/sessions/sessiondb/redis/service/config.go
Normal file
78
adaptors/sessions/sessiondb/redis/service/config.go
Normal file
|
@ -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
|
||||
}
|
272
adaptors/sessions/sessiondb/redis/service/service.go
Normal file
272
adaptors/sessions/sessiondb/redis/service/service.go
Normal file
|
@ -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
|
||||
}
|
188
adaptors/sessions/sessions.go
Normal file
188
adaptors/sessions/sessions.go
Normal file
|
@ -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))
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
73
context.go
73
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
50
iris.go
50
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 (
|
||||
|
@ -72,7 +71,6 @@ type Framework struct {
|
|||
|
||||
once sync.Once
|
||||
Config *Configuration
|
||||
sessions sessions.Sessions
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
-------------------------------------------------------------------
|
||||
|
|
95
policy.go
95
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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user