mirror of
https://github.com/kataras/iris.git
synced 2026-01-23 20:05:59 +00:00
Publish the new version ✈️ | Look description please!
# FAQ ### Looking for free support? http://support.iris-go.com https://kataras.rocket.chat/channel/iris ### Looking for previous versions? https://github.com/kataras/iris#version ### Should I upgrade my Iris? Developers are not forced to upgrade if they don't really need it. Upgrade whenever you feel ready. > Iris uses the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature, so you get truly reproducible builds, as this method guards against upstream renames and deletes. **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris`. For further installation support, please click [here](http://support.iris-go.com/d/16-how-to-install-iris-web-framework). ### About our new home page http://iris-go.com Thanks to [Santosh Anand](https://github.com/santoshanand) the http://iris-go.com has been upgraded and it's really awesome! [Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him. The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please! Read more at https://github.com/kataras/iris/blob/master/HISTORY.md Former-commit-id: eec2d71bbe011d6b48d2526eb25919e36e5ad94e
This commit is contained in:
27
sessions/LICENSE
Normal file
27
sessions/LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Gerasimos Maropoulos nor the name of his
|
||||
username, kataras, may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
98
sessions/config.go
Normal file
98
sessions/config.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
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. Please review it well before using sessions.
|
||||
Config struct {
|
||||
// Cookie string, the session's client cookie name, for example: "mysessionid"
|
||||
//
|
||||
// Defaults to "irissessionid"
|
||||
Cookie string
|
||||
|
||||
// CookieSecureTLS set to true if server is running over TLS
|
||||
// and you need the session's cookie "Secure" field to be setted true.
|
||||
//
|
||||
// Note: The user should fill the Decode configuation field in order for this to work.
|
||||
// Recommendation: You don't need this to be setted to true, just fill the Encode and Decode fields
|
||||
// with a third-party library like secure cookie, example is provided at the _examples folder.
|
||||
//
|
||||
// Defaults to false
|
||||
CookieSecureTLS bool
|
||||
|
||||
// Encode the cookie value if not nil.
|
||||
// Should accept as first argument the cookie name (config.Name)
|
||||
// as second argument the server's generated session id.
|
||||
// Should return the new session id, if error the session id setted to empty which is invalid.
|
||||
//
|
||||
// Note: Errors are not printed, so you have to know what you're doing,
|
||||
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
|
||||
// You either need to provide exactly that amount or you derive the key from what you type in.
|
||||
//
|
||||
// Defaults to nil
|
||||
Encode func(cookieName string, value interface{}) (string, error)
|
||||
// Decode the cookie value if not nil.
|
||||
// Should accept as first argument the cookie name (config.Name)
|
||||
// as second second accepts the client's cookie value (the encoded session id).
|
||||
// Should return an error if decode operation failed.
|
||||
//
|
||||
// Note: Errors are not printed, so you have to know what you're doing,
|
||||
// and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
|
||||
// You either need to provide exactly that amount or you derive the key from what you type in.
|
||||
//
|
||||
// Defaults to nil
|
||||
Decode func(cookieName string, cookieValue string, v interface{}) error
|
||||
|
||||
// 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
|
||||
|
||||
// SessionIDGenerator should returns a random session id.
|
||||
// By default we will use a uuid impl package to generate
|
||||
// that, but developers can change that with simple assignment.
|
||||
SessionIDGenerator func() string
|
||||
|
||||
// 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.SessionIDGenerator == nil {
|
||||
c.SessionIDGenerator = func() string {
|
||||
id := uuid.NewV4()
|
||||
return id.String()
|
||||
}
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
82
sessions/cookie.go
Normal file
82
sessions/cookie.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"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 persistence,
|
||||
// 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
|
||||
}
|
||||
21
sessions/database.go
Normal file
21
sessions/database.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
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{})
|
||||
}
|
||||
128
sessions/provider.go
Normal file
128
sessions/provider.go
Normal file
@@ -0,0 +1,128 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
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) 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) 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()
|
||||
|
||||
}
|
||||
319
sessions/session.go
Normal file
319
sessions/session.go
Normal file
@@ -0,0 +1,319 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/core/errors"
|
||||
)
|
||||
|
||||
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 context.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 _ 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if vint, ok := v.(int); ok {
|
||||
return int64(vint), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if vfloat64, ok := v.(float64); ok {
|
||||
return float32(vfloat64), nil
|
||||
}
|
||||
|
||||
if vint, ok := v.(int); ok {
|
||||
return float32(vint), nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if vfloat64, ok := v.(float64); ok {
|
||||
return vfloat64, nil
|
||||
}
|
||||
|
||||
if vint, ok := v.(int); ok {
|
||||
return float64(vint), nil
|
||||
}
|
||||
|
||||
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
sessions/sessiondb/README.md
Normal file
29
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")
|
||||
```
|
||||
91
sessions/sessiondb/redis/database.go
Normal file
91
sessions/sessiondb/redis/database.go
Normal file
@@ -0,0 +1,91 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package redis
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/kataras/iris/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
|
||||
///TODO: Find a way to use the iris' defined logger via an optional interface to Database.
|
||||
// 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 removed because of previous TODO
|
||||
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
|
||||
}
|
||||
82
sessions/sessiondb/redis/service/config.go
Normal file
82
sessions/sessiondb/redis/service/config.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
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
|
||||
}
|
||||
276
sessions/sessiondb/redis/service/service.go
Normal file
276
sessions/sessiondb/redis/service/service.go
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"github.com/kataras/iris/core/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
|
||||
}
|
||||
219
sessions/sessions.go
Normal file
219
sessions/sessions.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package sessions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
// Sessions must be implemented within a session manager.
|
||||
//
|
||||
// A Sessions should be responsible to Start a sesion based
|
||||
// on raw http.ResponseWriter and http.Request, which should return
|
||||
// a compatible Session interface, type. If the external session manager
|
||||
// doesn't qualifies, then the user should code the rest of the functions with empty implementation.
|
||||
//
|
||||
// Sessions should be responsible to Destroy a session based
|
||||
// on the http.ResponseWriter and http.Request, this function should works individually.
|
||||
Sessions interface {
|
||||
// Start should start the session for the particular net/http request.
|
||||
Start(http.ResponseWriter, *http.Request) Session
|
||||
|
||||
// Destroy should kills the net/http session and remove the associated cookie.
|
||||
Destroy(http.ResponseWriter, *http.Request)
|
||||
} // Sessions is being implemented by Manager
|
||||
|
||||
// Session should expose the Sessions'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()
|
||||
} // Session is being implemented inside session.go
|
||||
|
||||
// Manager implements the Sessions interface which Iris uses to start and destroy a session from its Context.
|
||||
Manager 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) *Manager {
|
||||
return &Manager{
|
||||
config: cfg.Validate(),
|
||||
provider: newProvider(),
|
||||
}
|
||||
}
|
||||
|
||||
var _ Sessions = &Manager{}
|
||||
|
||||
// UseDatabase adds a session database to the manager's provider,
|
||||
// a session db doesn't have write access
|
||||
func (s *Manager) UseDatabase(db Database) {
|
||||
s.provider.RegisterDatabase(db)
|
||||
}
|
||||
|
||||
// Start starts the session for the particular net/http request
|
||||
func (s *Manager) Start(res http.ResponseWriter, req *http.Request) Session {
|
||||
var sess Session
|
||||
|
||||
cookieValue := GetCookie(s.config.Cookie, req)
|
||||
|
||||
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
|
||||
sid := s.config.SessionIDGenerator()
|
||||
|
||||
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 persistence
|
||||
}
|
||||
|
||||
}
|
||||
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())
|
||||
}
|
||||
|
||||
// set the cookie to secure if this is a tls wrapped request
|
||||
// and the configuration allows it.
|
||||
if req.TLS != nil && s.config.CookieSecureTLS {
|
||||
cookie.Secure = true
|
||||
}
|
||||
|
||||
// encode the session id cookie client value right before send it.
|
||||
cookie.Value = s.encodeCookieValue(cookie.Value)
|
||||
|
||||
AddCookie(cookie, res)
|
||||
} else {
|
||||
|
||||
cookieValue = s.decodeCookieValue(cookieValue)
|
||||
|
||||
sess = s.provider.Read(cookieValue, s.config.Expires)
|
||||
}
|
||||
return sess
|
||||
}
|
||||
|
||||
// Destroy remove the session data and remove the associated cookie.
|
||||
func (s *Manager) Destroy(res http.ResponseWriter, req *http.Request) {
|
||||
cookieValue := GetCookie(s.config.Cookie, req)
|
||||
// decode the client's cookie value in order to find the server's session id
|
||||
// to destroy the session data.
|
||||
cookieValue = s.decodeCookieValue(cookieValue)
|
||||
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.
|
||||
//
|
||||
// Note: the sid should be the original one (i.e: fetched by a store )
|
||||
// it's not decoded.
|
||||
func (s *Manager) 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.
|
||||
func (s *Manager) DestroyAll() {
|
||||
s.provider.DestroyAll()
|
||||
}
|
||||
|
||||
// let's keep these funcs simple, we can do it with two lines but we may add more things in the future.
|
||||
func (s *Manager) decodeCookieValue(cookieValue string) string {
|
||||
var cookieValueDecoded *string
|
||||
if decode := s.config.Decode; decode != nil {
|
||||
err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded)
|
||||
if err == nil {
|
||||
cookieValue = *cookieValueDecoded
|
||||
} else {
|
||||
cookieValue = ""
|
||||
}
|
||||
}
|
||||
return cookieValue
|
||||
}
|
||||
|
||||
func (s *Manager) encodeCookieValue(cookieValue string) string {
|
||||
if encode := s.config.Encode; encode != nil {
|
||||
newVal, err := encode(s.config.Cookie, cookieValue)
|
||||
if err == nil {
|
||||
cookieValue = newVal
|
||||
} else {
|
||||
cookieValue = ""
|
||||
}
|
||||
}
|
||||
|
||||
return cookieValue
|
||||
}
|
||||
Reference in New Issue
Block a user