SessionsPolicy and sessions adaptor, history and _example written.

Former-commit-id: e8b0dde3cb3b72919f01b9d836d8ccb3d4e20214
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-02-15 20:06:19 +02:00
parent 82afcc5aa6
commit 13e83fc57e
21 changed files with 1729 additions and 141 deletions

View File

@ -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
View 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.

View 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")
}

View 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
View 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
}

View 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{})
}

View 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()
}

View 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()
}

View 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")
```

View 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
}

View 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
}

View 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
}

View 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))
}

View File

@ -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 (

View File

@ -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 (

View File

@ -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 (

View File

@ -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

View File

@ -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)
}
}

54
iris.go
View File

@ -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")
}
-------------------------------------------------------------------

View File

@ -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
}
}

View File

@ -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")
}