1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +00:00

Add notes for the new lead maintainer of the open-source iris project and align with @get-ion/ion by @hiveminded

Former-commit-id: da4f38eb9034daa49446df3ee529423b98f9b331
This commit is contained in:
kataras
2017-07-10 18:32:42 +03:00
parent 2d4c2779a7
commit 9f85b74fc9
344 changed files with 4842 additions and 5174 deletions

5
sessions/AUTHORS Normal file
View File

@@ -0,0 +1,5 @@
# This is the official list of Iris Sessions authors for copyright
# purposes.
Gerasimos Maropoulos <kataras2006@hotmail.com>
Bill Qeras, Jr. <hiveminded@tutanota.com>

View File

@@ -1,4 +1,4 @@
Copyright (c) 2017 Gerasimos Maropoulos, ΓΜ. All rights reserved.
Copyright (c) 2017 The Iris Sessions Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@@ -10,8 +10,8 @@ notice, this list of conditions and the following disclaimer.
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
* Neither the name of Iris nor the names of its
contributors 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

11
sessions/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Sessions
Fast HTTP Sessions for the [iris](https://github.com/kataras/iris) web framework.
## Table of contents
* [Overview](_examples/overview/main.go)
* [Standalone](_examples/standalone/main.go)
* [Secure Cookie](_examples/securecookie/main.go)
* [Flash Messages](_examples/flash-messages/main.go)
* [Database](_examples/database/main.go)

View File

@@ -0,0 +1,69 @@
package main
import (
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/sessions/sessiondb/redis"
"github.com/kataras/iris/sessions/sessiondb/redis/service"
)
func main() {
// replace with your running redis' server settings:
db := redis.New(service.Config{Network: service.DefaultRedisNetwork,
Addr: service.DefaultRedisAddr,
Password: "",
Database: "",
MaxIdle: 0,
MaxActive: 0,
IdleTimeout: service.DefaultRedisIdleTimeout,
Prefix: "",
MaxAgeSeconds: service.DefaultRedisMaxAgeSeconds}) // optionally configure the bridge between your redis server
sess := sessions.New(sessions.Config{Cookie: "sessionscookieid"})
//
// IMPORTANT:
//
sess.UseDatabase(db)
// the rest of the code stays the same.
app := iris.New()
app.Get("/", func(ctx context.Context) {
ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead")
})
app.Get("/set", func(ctx context.Context) {
//set session values
sess.Start(ctx).Set("name", "iris")
//test if setted here
ctx.Writef("All ok session setted to: %s", sess.Start(ctx).GetString("name"))
})
app.Get("/get", func(ctx context.Context) {
// get a specific key, as string, if no found returns just an empty string
name := sess.Start(ctx).GetString("name")
ctx.Writef("The name on the /set was: %s", name)
})
app.Get("/delete", func(ctx context.Context) {
// delete a specific key
sess.Start(ctx).Delete("name")
})
app.Get("/clear", func(ctx context.Context) {
// removes all entries
sess.Start(ctx).Clear()
})
app.Get("/destroy", func(ctx context.Context) {
//destroy, removes the entire session data and cookie
sess.Destroy(ctx)
})
app.Run(iris.Addr(":8080"))
}

View File

@@ -0,0 +1,43 @@
package main
import (
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
)
func main() {
app := iris.New()
sess := sessions.New(sessions.Config{Cookie: "myappsessionid"})
app.Get("/set", func(ctx context.Context) {
s := sess.Start(ctx)
s.SetFlash("name", "iris")
ctx.Writef("Message setted, is available for the next request")
})
app.Get("/get", func(ctx context.Context) {
s := sess.Start(ctx)
name := s.GetFlashString("name")
if name == "" {
ctx.Writef("Empty name!!")
return
}
ctx.Writef("Hello %s", name)
})
app.Get("/test", func(ctx context.Context) {
s := sess.Start(ctx)
name := s.GetFlashString("name")
if name == "" {
ctx.Writef("Empty name!!")
return
}
ctx.Writef("Ok you are coming from /set ,the value of the name is %s", name)
ctx.Writef(", and again from the same context: %s", name)
})
app.Run(iris.Addr(":8080"))
}

View File

@@ -0,0 +1,52 @@
package main
import (
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
)
var (
cookieNameForSessionID = "mycookiesessionnameid"
sess = sessions.New(sessions.Config{Cookie: cookieNameForSessionID})
)
func secret(ctx context.Context) {
// Check if user is authenticated
if auth, _ := sess.Start(ctx).GetBoolean("authenticated"); !auth {
ctx.StatusCode(iris.StatusForbidden)
return
}
// Print secret message
ctx.WriteString("The cake is a lie!")
}
func login(ctx context.Context) {
session := sess.Start(ctx)
// Authentication goes here
// ...
// Set user as authenticated
session.Set("authenticated", true)
}
func logout(ctx context.Context) {
session := sess.Start(ctx)
// Revoke users authentication
session.Set("authenticated", false)
}
func main() {
app := iris.New()
app.Get("/secret", secret)
app.Get("/login", login)
app.Get("/logout", logout)
app.Run(iris.Addr(":8080"))
}

View File

@@ -0,0 +1,75 @@
package main
// developers can use any library to add a custom cookie encoder/decoder.
// At this example we use the gorilla's securecookie package:
// $ go get github.com/gorilla/securecookie
// $ go run main.go
import (
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
"github.com/gorilla/securecookie"
)
func main() {
app := iris.New()
cookieName := "mycustomsessionid"
// AES 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.
hashKey := []byte("the-big-and-secret-fash-key-here")
blockKey := []byte("lot-secret-of-characters-big-too")
secureCookie := securecookie.New(hashKey, blockKey)
mySessions := sessions.New(sessions.Config{
Cookie: cookieName,
Encode: secureCookie.Encode,
Decode: secureCookie.Decode,
})
app.Get("/", func(ctx context.Context) {
ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead")
})
app.Get("/set", func(ctx context.Context) {
//set session values
s := mySessions.Start(ctx)
s.Set("name", "iris")
//test if setted here
ctx.Writef("All ok session setted to: %s", s.GetString("name"))
})
app.Get("/get", func(ctx context.Context) {
// get a specific key, as string, if no found returns just an empty string
s := mySessions.Start(ctx)
name := s.GetString("name")
ctx.Writef("The name on the /set was: %s", name)
})
app.Get("/delete", func(ctx context.Context) {
// delete a specific key
s := mySessions.Start(ctx)
s.Delete("name")
})
app.Get("/clear", func(ctx context.Context) {
// removes all entries
mySessions.Start(ctx).Clear()
})
app.Get("/destroy", func(ctx context.Context) {
//destroy, removes the entire session data and cookie
mySessions.Destroy(ctx)
}) // Note about destroy:
//
// You can destroy a session outside of a handler too, using the:
// mySessions.DestroyByID
// mySessions.DestroyAll
app.Run(iris.Addr(":8080"))
}

View File

@@ -0,0 +1,105 @@
package main
import (
"testing"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/httptest"
"github.com/kataras/iris/sessions"
"github.com/gorilla/securecookie"
)
func TestSessionsEncodeDecode(t *testing.T) {
// test the sessions encode decode via gorilla.securecookie
app := iris.New()
// IMPORTANT
cookieName := "mycustomsessionid"
// AES 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.
hashKey := []byte("the-big-and-secret-fash-key-here")
blockKey := []byte("lot-secret-of-characters-big-too")
secureCookie := securecookie.New(hashKey, blockKey)
sess := sessions.New(sessions.Config{
Cookie: cookieName,
Encode: secureCookie.Encode,
Decode: secureCookie.Decode,
})
testSessions(t, sess, app)
}
func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application) {
values := map[string]interface{}{
"Name": "iris",
"Months": "4",
"Secret": "dsads£2132215£%%Ssdsa",
}
writeValues := func(ctx context.Context) {
s := sess.Start(ctx)
sessValues := s.GetAll()
ctx.JSON(sessValues)
}
app.Post("/set", func(ctx context.Context) {
s := sess.Start(ctx)
vals := make(map[string]interface{}, 0)
if err := ctx.ReadJSON(&vals); err != nil {
t.Fatalf("Cannot readjson. Trace %s", err.Error())
}
for k, v := range vals {
s.Set(k, v)
}
})
app.Get("/get", func(ctx context.Context) {
writeValues(ctx)
})
app.Get("/clear", func(ctx context.Context) {
sess.Start(ctx).Clear()
writeValues(ctx)
})
app.Get("/destroy", func(ctx context.Context) {
sess.Destroy(ctx)
writeValues(ctx)
// the cookie and all values should be empty
})
// request cookie should be empty
app.Get("/after_destroy", func(ctx context.Context) {
})
app.Get("/multi_start_set_get", func(ctx context.Context) {
s := sess.Start(ctx)
s.Set("key", "value")
ctx.Next()
}, func(ctx context.Context) {
s := sess.Start(ctx)
ctx.Writef(s.GetString("key"))
})
e := httptest.New(t, app, httptest.URL("http://example.com"))
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty()
e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values)
// test destroy which also clears first
d := e.GET("/destroy").Expect().Status(iris.StatusOK)
d.JSON().Object().Empty()
// This removed: d.Cookies().Empty(). Reason:
// httpexpect counts the cookies setted or deleted at the response time, but cookie is not removed, to be really removed needs to SetExpire(now-1second) so,
// test if the cookies removed on the next request, like the browser's behavior.
e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty()
// set and clear again
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty()
e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty()
// test start on the same request but more than one times
e.GET("/multi_start_set_get").Expect().Status(iris.StatusOK).Body().Equal("value")
}

View File

@@ -0,0 +1,120 @@
package main
import (
"time"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/sessions"
)
type businessModel struct {
Name string
}
func main() {
app := iris.New()
sess := sessions.New(sessions.Config{
// Cookie string, the session's client cookie name, for example: "mysessionid"
//
// Defaults to "irissessionid"
Cookie: "mysessionid",
// 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,
// if you want to invalid cookies on different subdomains
// of the same host, then enable it
DisableSubdomainPersistence: false,
// want to be crazy safe? Take a look at the "securecookie" example folder.
})
app.Get("/", func(ctx context.Context) {
ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead")
})
app.Get("/set", func(ctx context.Context) {
//set session values.
s := sess.Start(ctx)
s.Set("name", "iris")
//test if setted here
ctx.Writef("All ok session setted to: %s", s.GetString("name"))
// Set will set the value as-it-is,
// if it's a slice or map
// you will be able to change it on .Get directly!
// Keep note that I don't recommend saving big data neither slices or maps on a session
// but if you really need it then use the `SetImmutable` instead of `Set`.
// Use `SetImmutable` consistently, it's slower.
// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081
})
app.Get("/get", func(ctx context.Context) {
// get a specific value, as string, if no found returns just an empty string
name := sess.Start(ctx).GetString("name")
ctx.Writef("The name on the /set was: %s", name)
})
app.Get("/delete", func(ctx context.Context) {
// delete a specific key
sess.Start(ctx).Delete("name")
})
app.Get("/clear", func(ctx context.Context) {
// removes all entries
sess.Start(ctx).Clear()
})
app.Get("/destroy", func(ctx context.Context) {
//destroy, removes the entire session data and cookie
sess.Destroy(ctx)
})
// Note about Destroy:
//
// You can destroy a session outside of a handler too, using the:
// mySessions.DestroyByID
// mySessions.DestroyAll
// remember: slices and maps are muttable by-design
// The `SetImmutable` makes sure that they will be stored and received
// as immutable, so you can't change them directly by mistake.
//
// Use `SetImmutable` consistently, it's slower than `Set`.
// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081
app.Get("/set_immutable", func(ctx context.Context) {
business := []businessModel{{Name: "Edward"}, {Name: "value 2"}}
s := sess.Start(ctx)
s.SetImmutable("businessEdit", business)
businessGet := s.Get("businessEdit").([]businessModel)
// try to change it, if we used `Set` instead of `SetImmutable` this
// change will affect the underline array of the session's value "businessEdit", but now it will not.
businessGet[0].Name = "Gabriel"
})
app.Get("/get_immutable", func(ctx context.Context) {
valSlice := sess.Start(ctx).Get("businessEdit")
if valSlice == nil {
ctx.HTML("please navigate to the <a href='/set_immutable'>/set_immutable</a> first")
return
}
firstModel := valSlice.([]businessModel)[0]
// businessGet[0].Name is equal to Edward initially
if firstModel.Name != "Edward" {
panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable")
}
ctx.Writef("[]businessModel[0].Name remains: %s", firstModel.Name)
// the name should remains "Edward"
})
app.Run(iris.Addr(":8080"))
}

View File

@@ -1,7 +1,3 @@
// 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 (
@@ -13,8 +9,6 @@ import (
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 (
@@ -73,7 +67,7 @@ type (
// 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
// DisableSubdomainPersistence set it to true in order dissallow your subdomains to have access to the session cookie
//
// Defaults to false
DisableSubdomainPersistence bool

View File

@@ -1,7 +1,3 @@
// 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 (
@@ -9,6 +5,8 @@ import (
"strconv"
"strings"
"time"
"github.com/kataras/iris/context"
)
var (
@@ -21,24 +19,27 @@ var (
// 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)
func GetCookie(ctx context.Context, name string) string {
c, err := ctx.Request().Cookie(name)
if err != nil {
return ""
}
return c.Value
// return ctx.GetCookie(name)
}
// AddCookie adds a cookie
func AddCookie(cookie *http.Cookie, res http.ResponseWriter) {
if v := cookie.String(); v != "" {
http.SetCookie(res, cookie)
}
func AddCookie(ctx context.Context, cookie *http.Cookie) {
// http.SetCookie(ctx.ResponseWriter(), cookie)
// ctx.Request().AddCookie(cookie)
ctx.SetCookie(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)
func RemoveCookie(ctx context.Context, name string) {
c, err := ctx.Request().Cookie(name)
if err != nil {
return
}
@@ -48,7 +49,7 @@ func RemoveCookie(name string, res http.ResponseWriter, req *http.Request) {
c.MaxAge = -1
c.Value = ""
c.Path = "/"
AddCookie(c, res)
AddCookie(ctx, c)
}
// IsValidCookieDomain returns true if the receiver is a valid domain to set

View File

@@ -1,7 +1,3 @@
// 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
@@ -10,7 +6,7 @@ package sessions
// 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.
// 'this session value should added' are made automatically 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

View File

@@ -1,7 +1,3 @@
// 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 (
@@ -16,10 +12,10 @@ type (
// 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)
// (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
sessions map[string]*Session
databases []Database
}
)
@@ -27,7 +23,7 @@ type (
// newProvider returns a new sessions provider
func newProvider() *provider {
return &provider{
sessions: make(map[string]*session, 0),
sessions: make(map[string]*Session, 0),
databases: make([]Database, 0),
}
}
@@ -41,9 +37,8 @@ func (p *provider) RegisterDatabase(db Database) {
}
// newSession returns a new session from sessionid
func (p *provider) newSession(sid string, expires time.Duration) *session {
sess := &session{
func (p *provider) newSession(sid string, expires time.Duration) *Session {
sess := &Session{
sid: sid,
provider: p,
values: p.loadSessionValuesFromDB(sid),
@@ -92,7 +87,7 @@ func (p *provider) updateDatabases(sid string, store memstore.Store) {
}
// Init creates the session and returns it
func (p *provider) Init(sid string, expires time.Duration) Session {
func (p *provider) Init(sid string, expires time.Duration) *Session {
newSession := p.newSession(sid, expires)
p.mu.Lock()
p.sessions[sid] = newSession
@@ -101,7 +96,7 @@ func (p *provider) Init(sid string, expires time.Duration) Session {
}
// Read returns the store which sid parameter belongs
func (p *provider) Read(sid string, expires time.Duration) Session {
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

View File

@@ -1,7 +1,3 @@
// 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 (
@@ -14,10 +10,12 @@ import (
)
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 {
// Session should expose the Sessions's end-user API.
// It is the session's storage controller which you can
// save or retrieve values based on a key.
//
// This is what will be returned when sess := sessions.Start().
Session struct {
sid string
values memstore.Store // here are the real values
// we could set the flash messages inside values but this will bring us more problems
@@ -39,15 +37,13 @@ type (
}
)
var _ Session = &session{}
// ID returns the session's id
func (s *session) ID() string {
// ID returns the session's ID.
func (s *Session) ID() string {
return s.sid
}
// Get returns a value based on its "key".
func (s *session) Get(key string) interface{} {
func (s *Session) Get(key string) interface{} {
s.mu.RLock()
value := s.values.Get(key)
s.mu.RUnlock()
@@ -55,8 +51,8 @@ func (s *session) Get(key string) interface{} {
return value
}
// when running on the session manager removes any 'old' flash messages
func (s *session) runFlashGC() {
// 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 {
@@ -67,7 +63,7 @@ func (s *session) runFlashGC() {
}
// HasFlash returns true if this session has available flash messages.
func (s *session) HasFlash() bool {
func (s *Session) HasFlash() bool {
return len(s.flashes) > 0
}
@@ -80,7 +76,7 @@ func (s *session) HasFlash() bool {
//
// 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) interface{} {
func (s *Session) GetFlash(key string) interface{} {
fv, ok := s.peekFlashMessage(key)
if !ok {
return nil
@@ -92,7 +88,7 @@ func (s *session) GetFlash(key string) interface{} {
// PeekFlash returns a stored flash message based on its "key".
// Unlike GetFlash, this will keep the message valid for the next requests,
// until GetFlashes or GetFlash("key").
func (s *session) PeekFlash(key string) interface{} {
func (s *Session) PeekFlash(key string) interface{} {
fv, ok := s.peekFlashMessage(key)
if !ok {
return nil
@@ -100,7 +96,7 @@ func (s *session) PeekFlash(key string) interface{} {
return fv.value
}
func (s *session) peekFlashMessage(key string) (*flashMessage, bool) {
func (s *Session) peekFlashMessage(key string) (*flashMessage, bool) {
s.mu.Lock()
if fv, found := s.flashes[key]; found {
return fv, true
@@ -110,8 +106,8 @@ func (s *session) peekFlashMessage(key string) (*flashMessage, bool) {
return nil, false
}
// GetString same as Get but returns as string, if nil then returns an empty string
func (s *session) GetString(key string) string {
// 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
@@ -121,8 +117,8 @@ func (s *session) GetString(key string) string {
return ""
}
// GetFlashString same as GetFlash but returns as string, if nil then returns an empty string
func (s *session) GetFlashString(key string) string {
// 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
@@ -134,8 +130,8 @@ func (s *session) GetFlashString(key string) string {
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) {
// 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 {
@@ -149,8 +145,8 @@ func (s *session) GetInt(key string) (int, error) {
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) {
// 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 {
@@ -169,8 +165,8 @@ func (s *session) GetInt64(key string) (int64, error) {
}
// 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) {
// 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 {
@@ -196,8 +192,8 @@ func (s *session) GetFloat32(key string) (float32, error) {
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) {
// 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 {
@@ -220,11 +216,11 @@ func (s *session) GetFloat64(key string) (float64, error) {
}
// 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) {
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
// 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
}
@@ -232,8 +228,8 @@ func (s *session) GetBoolean(key string) (bool, error) {
return false, errFindParse.Format("bool", key, v)
}
// GetAll returns a copy of all session's values
func (s *session) GetAll() map[string]interface{} {
// 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 _, kv := range s.values {
@@ -244,8 +240,8 @@ func (s *session) GetAll() map[string]interface{} {
}
// 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{} {
// 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 {
@@ -257,11 +253,11 @@ func (s *session) GetFlashes() map[string]interface{} {
}
// VisitAll loop each one entry and calls the callback function func(key,value)
func (s *session) VisitAll(cb func(k string, v interface{})) {
func (s *Session) VisitAll(cb func(k string, v interface{})) {
s.values.Visit(cb)
}
func (s *session) set(key string, value interface{}, immutable bool) {
func (s *Session) set(key string, value interface{}, immutable bool) {
s.mu.Lock()
if immutable {
s.values.SetImmutable(key, value)
@@ -274,7 +270,7 @@ func (s *session) set(key string, value interface{}, immutable bool) {
}
// Set fills the session with an entry"value", based on its "key".
func (s *session) Set(key string, value interface{}) {
func (s *Session) Set(key string, value interface{}) {
s.set(key, value, false)
}
@@ -284,7 +280,7 @@ func (s *session) Set(key string, value interface{}) {
// if the entry was immutable, for your own safety.
// Use it consistently, it's far slower than `Set`.
// Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081
func (s *session) SetImmutable(key string, value interface{}) {
func (s *Session) SetImmutable(key string, value interface{}) {
s.set(key, value, true)
}
@@ -307,8 +303,8 @@ func (s *session) SetImmutable(key string, value interface{}) {
// 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{}) {
// 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()
@@ -316,7 +312,7 @@ func (s *session) SetFlash(key string, value interface{}) {
// Delete removes an entry by its key,
// returns true if actually something was removed.
func (s *session) Delete(key string) bool {
func (s *Session) Delete(key string) bool {
s.mu.Lock()
removed := s.values.Remove(key)
s.mu.Unlock()
@@ -325,19 +321,19 @@ func (s *session) Delete(key string) bool {
return removed
}
func (s *session) 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) {
// 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() {
// Clear removes all entries.
func (s *Session) Clear() {
s.mu.Lock()
s.values.Reset()
s.mu.Unlock()
@@ -345,8 +341,8 @@ func (s *session) Clear() {
s.updateDatabases()
}
// Clear removes all flash messages
func (s *session) ClearFlashes() {
// ClearFlashes removes all flash messages.
func (s *Session) ClearFlashes() {
s.mu.Lock()
for key := range s.flashes {
delete(s.flashes, key)

View File

@@ -1,29 +0,0 @@
## 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

@@ -1,7 +1,3 @@
// 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 (
@@ -11,22 +7,22 @@ import (
"github.com/kataras/iris/sessions/sessiondb/redis/service"
)
// Database the redis database for q sessions
// Database the redis back-end session database for the sessions.
type Database struct {
redis *service.Service
}
// New returns a new redis database
// 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
// 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
// Load loads the values to the underline.
func (d *Database) Load(sid string) map[string]interface{} {
values := make(map[string]interface{})

View File

@@ -1,13 +1,7 @@
// 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 (
@@ -43,7 +37,7 @@ type Config struct {
MaxAgeSeconds int
}
// DefaultConfig returns the default configuration for Redis service
// DefaultConfig returns the default configuration for Redis service.
func DefaultConfig() Config {
return Config{
Network: DefaultRedisNetwork,
@@ -57,26 +51,3 @@ func DefaultConfig() Config {
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

@@ -1,7 +1,3 @@
// 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 (
@@ -268,9 +264,12 @@ func (r *Service) Connect() {
}
// New returns a Redis service filled by the passed config
// to connect call the .Connect()
// to connect call the .Connect().
func New(cfg ...Config) *Service {
c := DefaultConfig().Merge(cfg)
c := DefaultConfig()
if len(cfg) > 0 {
c = cfg[0]
}
r := &Service{pool: &redis.Pool{}, Config: &c}
return r
}

View File

@@ -1,94 +1,48 @@
// 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"
"github.com/kataras/iris/context"
)
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{})
SetImmutable(key string, value interface{})
SetFlash(string, interface{})
Delete(string) bool
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
}
)
// A Sessions manager should be responsible to Start a sesion, based
// on a Context, 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 Context.
type 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) *Manager {
return &Manager{
// it can be adapted to an iris station
func New(cfg Config) *Sessions {
return &Sessions{
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) {
func (s *Sessions) 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)
// Start should start the session for the particular request.
func (s *Sessions) Start(ctx context.Context) *Session {
cookieValue := GetCookie(ctx, s.config.Cookie)
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)
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
@@ -98,7 +52,7 @@ func (s *Manager) Start(res http.ResponseWriter, req *http.Request) Session {
cookie.Path = "/"
if !s.config.DisableSubdomainPersistence {
requestDomain := req.URL.Host
requestDomain := ctx.Request().URL.Host
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
@@ -125,8 +79,8 @@ func (s *Manager) Start(res http.ResponseWriter, req *http.Request) Session {
// 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'
@@ -142,33 +96,33 @@ func (s *Manager) Start(res http.ResponseWriter, req *http.Request) Session {
// set the cookie to secure if this is a tls wrapped request
// and the configuration allows it.
if req.TLS != nil && s.config.CookieSecureTLS {
if ctx.Request().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(ctx, cookie)
AddCookie(cookie, res)
} else {
cookieValue = s.decodeCookieValue(cookieValue)
sess = s.provider.Read(cookieValue, s.config.Expires)
return sess
}
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)
func (s *Sessions) Destroy(ctx context.Context) {
cookieValue := GetCookie(ctx, s.config.Cookie)
// 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)
RemoveCookie(ctx, s.config.Cookie)
s.provider.Destroy(cookieValue)
}
@@ -181,19 +135,19 @@ func (s *Manager) Destroy(res http.ResponseWriter, req *http.Request) {
//
// Note: the sid should be the original one (i.e: fetched by a store )
// it's not decoded.
func (s *Manager) DestroyByID(sid string) {
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.
func (s *Manager) DestroyAll() {
func (s *Sessions) 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 {
func (s *Sessions) decodeCookieValue(cookieValue string) string {
var cookieValueDecoded *string
if decode := s.config.Decode; decode != nil {
err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded)
@@ -206,7 +160,7 @@ func (s *Manager) decodeCookieValue(cookieValue string) string {
return cookieValue
}
func (s *Manager) encodeCookieValue(cookieValue string) string {
func (s *Sessions) encodeCookieValue(cookieValue string) string {
if encode := s.config.Encode; encode != nil {
newVal, err := encode(s.config.Cookie, cookieValue)
if err == nil {

194
sessions/sessions_test.go Normal file
View File

@@ -0,0 +1,194 @@
package sessions_test
import (
"testing"
"github.com/kataras/iris"
"github.com/kataras/iris/context"
"github.com/kataras/iris/httptest"
"github.com/kataras/iris/sessions"
)
func TestSessions(t *testing.T) {
app := iris.New()
sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"})
testSessions(t, sess, app)
}
const (
testEnableSubdomain = false
)
func testSessions(t *testing.T, sess *sessions.Sessions, app *iris.Application) {
values := map[string]interface{}{
"Name": "iris",
"Months": "4",
"Secret": "dsads£2132215£%%Ssdsa",
}
writeValues := func(ctx context.Context) {
s := sess.Start(ctx)
sessValues := s.GetAll()
ctx.JSON(sessValues)
}
if testEnableSubdomain {
app.Party("subdomain.").Get("/get", func(ctx context.Context) {
writeValues(ctx)
})
}
app.Post("/set", func(ctx context.Context) {
s := sess.Start(ctx)
vals := make(map[string]interface{}, 0)
if err := ctx.ReadJSON(&vals); err != nil {
t.Fatalf("Cannot readjson. Trace %s", err.Error())
}
for k, v := range vals {
s.Set(k, v)
}
})
app.Get("/get", func(ctx context.Context) {
writeValues(ctx)
})
app.Get("/clear", func(ctx context.Context) {
sess.Start(ctx).Clear()
writeValues(ctx)
})
app.Get("/destroy", func(ctx context.Context) {
sess.Destroy(ctx)
writeValues(ctx)
// the cookie and all values should be empty
})
// request cookie should be empty
app.Get("/after_destroy", func(ctx context.Context) {
})
app.Get("/multi_start_set_get", func(ctx context.Context) {
s := sess.Start(ctx)
s.Set("key", "value")
ctx.Next()
}, func(ctx context.Context) {
s := sess.Start(ctx)
ctx.Writef(s.GetString("key"))
})
e := httptest.New(t, app, httptest.URL("http://example.com"))
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty()
e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values)
if testEnableSubdomain {
es := httptest.New(t, app, httptest.URL("http://subdomain.example.com"))
es.Request("GET", "/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values)
}
// test destroy which also clears first
d := e.GET("/destroy").Expect().Status(iris.StatusOK)
d.JSON().Object().Empty()
// This removed: d.Cookies().Empty(). Reason:
// httpexpect counts the cookies setted or deleted at the response time, but cookie is not removed, to be really removed needs to SetExpire(now-1second) so,
// test if the cookies removed on the next request, like the browser's behavior.
e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty()
// set and clear again
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty()
e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty()
// test start on the same request but more than one times
e.GET("/multi_start_set_get").Expect().Status(iris.StatusOK).Body().Equal("value")
}
func TestFlashMessages(t *testing.T) {
app := iris.New()
sess := sessions.New(sessions.Config{Cookie: "mycustomsessionid"})
valueSingleKey := "Name"
valueSingleValue := "iris-sessions"
values := map[string]interface{}{
valueSingleKey: valueSingleValue,
"Days": "1",
"Secret": "dsads£2132215£%%Ssdsa",
}
writeValues := func(ctx context.Context, values map[string]interface{}) error {
_, err := ctx.JSON(values)
return err
}
app.Post("/set", func(ctx context.Context) {
vals := make(map[string]interface{}, 0)
if err := ctx.ReadJSON(&vals); err != nil {
t.Fatalf("Cannot readjson. Trace %s", err.Error())
}
s := sess.Start(ctx)
for k, v := range vals {
s.SetFlash(k, v)
}
ctx.StatusCode(iris.StatusOK)
})
writeFlashValues := func(ctx context.Context) {
s := sess.Start(ctx)
flashes := s.GetFlashes()
if err := writeValues(ctx, flashes); err != nil {
t.Fatalf("While serialize the flash values: %s", err.Error())
}
}
app.Get("/get_single", func(ctx context.Context) {
s := sess.Start(ctx)
flashMsgString := s.GetFlashString(valueSingleKey)
ctx.WriteString(flashMsgString)
})
app.Get("/get", func(ctx context.Context) {
writeFlashValues(ctx)
})
app.Get("/clear", func(ctx context.Context) {
s := sess.Start(ctx)
s.ClearFlashes()
writeFlashValues(ctx)
})
app.Get("/destroy", func(ctx context.Context) {
sess.Destroy(ctx)
writeFlashValues(ctx)
ctx.StatusCode(iris.StatusOK)
// the cookie and all values should be empty
})
// request cookie should be empty
app.Get("/after_destroy", func(ctx context.Context) {
ctx.StatusCode(iris.StatusOK)
})
e := httptest.New(t, app, httptest.URL("http://example.com"))
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty()
// get all
e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values)
// get the same flash on other request should return nothing because the flash message is removed after fetch once
e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Empty()
// test destroy which also clears first
d := e.GET("/destroy").Expect().Status(iris.StatusOK)
d.JSON().Object().Empty()
e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty()
// set and clear again
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty()
e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty()
// set again in order to take the single one ( we don't test Cookies.NotEmpty because httpexpect default conf reads that from the request-only)
e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK)
e.GET("/get_single").Expect().Status(iris.StatusOK).Body().Equal(valueSingleValue)
}