This commit is contained in:
swh 2017-04-20 14:44:54 +08:00
parent 7ce48c6de5
commit 793a11a5b0
5 changed files with 61 additions and 323 deletions

45
doc.go
View file

@ -22,26 +22,26 @@ The key features are:
Let's start with an example that shows the sessions API in a nutshell:
import (
"net/http"
"github.com/gorilla/sessions"
"github.com/admpub/sessions"
"github.com/webx-top/echo"
)
var store = sessions.NewCookieStore([]byte("something-very-secret"))
func MyHandler(w http.ResponseWriter, r *http.Request) {
func MyHandler(ctx echo.Context) error {
// Get a session. We're ignoring the error resulted from decoding an
// existing session: Get() always returns a session, even if empty.
session, err := store.Get(r, "session-name")
session, err := store.Get(ctx, "session-name")
if err != nil {
http.Error(w, err.Error(), 500)
return
return err
}
// Set some session values.
session.Values["foo"] = "bar"
session.Values[42] = 43
// Save it before we write to the response/return from the handler.
session.Save(r, w)
session.Save(ctx)
return nil
}
First we initialize a session store calling NewCookieStore() and passing a
@ -72,12 +72,11 @@ Ruby On Rails a few years back. When we request a flash message, it is removed
from the session. To add a flash, call session.AddFlash(), and to get all
flashes, call session.Flashes(). Here is an example:
func MyHandler(w http.ResponseWriter, r *http.Request) {
func MyHandler(ctx echo.Context) error {
// Get a session.
session, err := store.Get(r, "session-name")
session, err := store.Get(ctx, "session-name")
if err != nil {
http.Error(w, err.Error(), 500)
return
return error
}
// Get the previously flashes, if any.
@ -87,7 +86,8 @@ flashes, call session.Flashes(). Here is an example:
// Set a new flash.
session.AddFlash("Hello, flash messages world!")
}
session.Save(r, w)
session.Save(ctx)
return nil
}
Flash messages are useful to set information to be read after a redirection,
@ -99,7 +99,8 @@ so it is easy to register new datatypes for storage in sessions:
import(
"encoding/gob"
"github.com/gorilla/sessions"
"github.com/admpub/sessions"
"github.com/webx-top/echo"
)
type Person struct {
@ -126,11 +127,10 @@ values of those types to and from our sessions.
Note that because session values are stored in a map[string]interface{}, there's
a need to type-assert data when retrieving it. We'll use the Person struct we registered above:
func MyHandler(w http.ResponseWriter, r *http.Request) {
session, err := store.Get(r, "session-name")
func MyHandler(ctx echo.Context) error {
session, err := store.Get(ctx, "session-name")
if err != nil {
http.Error(w, err.Error(), 500)
return
return err
}
// Retrieve our struct and type-assert it
@ -141,6 +141,8 @@ a need to type-assert data when retrieving it. We'll use the Person struct we re
}
// Now we can use our person object
return nil
}
By default, session cookies last for a month. This is probably too long for
@ -182,15 +184,16 @@ at once: it's sessions.Save(). Here's an example:
var store = sessions.NewCookieStore([]byte("something-very-secret"))
func MyHandler(w http.ResponseWriter, r *http.Request) {
func MyHandler(ctx echo.Context) error {
// Get a session and set a value.
session1, _ := store.Get(r, "session-one")
session1, _ := store.Get(ctx, "session-one")
session1.Values["foo"] = "bar"
// Get another session and set another value.
session2, _ := store.Get(r, "session-two")
session2, _ := store.Get(ctx, "session-two")
session2.Values[42] = 43
// Save all sessions.
sessions.Save(r, w)
sessions.Save(ctx)
return nil
}
This is possible because when we call Get() from a session store, it adds the

View file

@ -11,7 +11,6 @@ import (
"time"
"github.com/webx-top/echo"
"github.com/webx-top/echo/engine"
)
// Default flashes key.
@ -92,7 +91,7 @@ func (s *Session) AddFlash(value interface{}, vars ...string) {
// store.Save(request, response, session). You should call Save before writing to
// the response or returning from the handler.
func (s *Session) Save(ctx echo.Context) error {
return s.store.Save(ctx.Request(), ctx.Response(), s)
return s.store.Save(ctx, s)
}
// Name returns the name used to register the session.
@ -113,30 +112,26 @@ type sessionInfo struct {
e error
}
// contextKey is the type used to store the registry in the context.
type contextKey int
// registryKey is the key used to store the registry in the context.
const registryKey contextKey = 0
const registryKey = `webx:mw.sessions`
// GetRegistry returns a registry instance for the current request.
func GetRegistry(ctx echo.Context) *Registry {
r := ctx.Request()
registry := engine.Get(r, registryKey)
if registry != nil {
return registry.(*Registry)
registry, ok := ctx.Get(registryKey).(*Registry)
if ok {
return registry
}
newRegistry := &Registry{
request: r,
registry = &Registry{
context: ctx,
sessions: make(map[string]sessionInfo),
}
engine.Set(r, registryKey, newRegistry)
return newRegistry
ctx.Set(registryKey, registry)
return registry
}
// Registry stores sessions used during a request.
type Registry struct {
request engine.Request
context echo.Context
sessions map[string]sessionInfo
}
@ -147,7 +142,7 @@ func (s *Registry) Get(store Store, name string) (session *Session, err error) {
if info, ok := s.sessions[name]; ok {
session, err = info.s, info.e
} else {
session, err = store.New(s.request, name)
session, err = store.New(s.context, name)
session.name = name
s.sessions[name] = sessionInfo{s: session, e: err}
}
@ -156,14 +151,14 @@ func (s *Registry) Get(store Store, name string) (session *Session, err error) {
}
// Save saves all sessions registered for the current request.
func (s *Registry) Save(w engine.Response) error {
func (s *Registry) Save(ctx echo.Context) error {
var errMulti MultiError
for name, info := range s.sessions {
session := info.s
if session.store == nil {
errMulti = append(errMulti, fmt.Errorf(
"sessions: missing store for session %q", name))
} else if err := session.store.Save(s.request, w, session); err != nil {
} else if err := session.store.Save(ctx, session); err != nil {
errMulti = append(errMulti, fmt.Errorf(
"sessions: error saving session %q -- %v", name, err))
}
@ -182,7 +177,7 @@ func init() {
// Save saves all sessions used during the current request.
func Save(ctx echo.Context) error {
return GetRegistry(ctx).Save(ctx.Response())
return GetRegistry(ctx).Save(ctx)
}
// NewCookie returns an http.Cookie with the options set. It also sets
@ -208,6 +203,18 @@ func NewCookie(name, value string, options *Options) *http.Cookie {
return cookie
}
// SetCookie for echo
func SetCookie(ctx echo.Context, key string, value string, options *Options) {
ctx.SetCookie(
key, value,
options.MaxAge,
options.Path,
options.Domain,
options.Secure,
options.HttpOnly,
)
}
// Error ----------------------------------------------------------------------
// MultiError stores multiple errors.

View file

@ -1,197 +0,0 @@
// Copyright 2012 The Gorilla Authors. 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 (
"bytes"
"encoding/gob"
"net/http"
"testing"
)
// ----------------------------------------------------------------------------
// ResponseRecorder
// ----------------------------------------------------------------------------
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// ResponseRecorder is an implementation of http.ResponseWriter that
// records its mutations for later inspection in tests.
type ResponseRecorder struct {
Code int // the HTTP response code from WriteHeader
HeaderMap http.Header // the HTTP response headers
Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to
Flushed bool
}
// NewRecorder returns an initialized ResponseRecorder.
func NewRecorder() *ResponseRecorder {
return &ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
}
// DefaultRemoteAddr is the default remote address to return in RemoteAddr if
// an explicit DefaultRemoteAddr isn't set on ResponseRecorder.
const DefaultRemoteAddr = "1.2.3.4"
// Header returns the response headers.
func (rw *ResponseRecorder) Header() http.Header {
return rw.HeaderMap
}
// Write always succeeds and writes to rw.Body, if not nil.
func (rw *ResponseRecorder) Write(buf []byte) (int, error) {
if rw.Body != nil {
rw.Body.Write(buf)
}
if rw.Code == 0 {
rw.Code = http.StatusOK
}
return len(buf), nil
}
// WriteHeader sets rw.Code.
func (rw *ResponseRecorder) WriteHeader(code int) {
rw.Code = code
}
// Flush sets rw.Flushed to true.
func (rw *ResponseRecorder) Flush() {
rw.Flushed = true
}
// ----------------------------------------------------------------------------
type FlashMessage struct {
Type int
Message string
}
func TestFlashes(t *testing.T) {
var req *http.Request
var rsp *ResponseRecorder
var hdr http.Header
var err error
var ok bool
var cookies []string
var session *Session
var flashes []interface{}
store := NewCookieStore([]byte("secret-key"))
// Round 1 ----------------------------------------------------------------
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
rsp = NewRecorder()
// Get a session.
if session, err = store.Get(req, "session-key"); err != nil {
t.Fatalf("Error getting session: %v", err)
}
// Get a flash.
flashes = session.Flashes()
if len(flashes) != 0 {
t.Errorf("Expected empty flashes; Got %v", flashes)
}
// Add some flashes.
session.AddFlash("foo")
session.AddFlash("bar")
// Custom key.
session.AddFlash("baz", "custom_key")
// Save.
if err = Save(req, rsp); err != nil {
t.Fatalf("Error saving session: %v", err)
}
hdr = rsp.Header()
cookies, ok = hdr["Set-Cookie"]
if !ok || len(cookies) != 1 {
t.Fatal("No cookies. Header:", hdr)
}
// Round 2 ----------------------------------------------------------------
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
req.Header.Add("Cookie", cookies[0])
rsp = NewRecorder()
// Get a session.
if session, err = store.Get(req, "session-key"); err != nil {
t.Fatalf("Error getting session: %v", err)
}
// Check all saved values.
flashes = session.Flashes()
if len(flashes) != 2 {
t.Fatalf("Expected flashes; Got %v", flashes)
}
if flashes[0] != "foo" || flashes[1] != "bar" {
t.Errorf("Expected foo,bar; Got %v", flashes)
}
flashes = session.Flashes()
if len(flashes) != 0 {
t.Errorf("Expected dumped flashes; Got %v", flashes)
}
// Custom key.
flashes = session.Flashes("custom_key")
if len(flashes) != 1 {
t.Errorf("Expected flashes; Got %v", flashes)
} else if flashes[0] != "baz" {
t.Errorf("Expected baz; Got %v", flashes)
}
flashes = session.Flashes("custom_key")
if len(flashes) != 0 {
t.Errorf("Expected dumped flashes; Got %v", flashes)
}
// Round 3 ----------------------------------------------------------------
// Custom type
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
rsp = NewRecorder()
// Get a session.
if session, err = store.Get(req, "session-key"); err != nil {
t.Fatalf("Error getting session: %v", err)
}
// Get a flash.
flashes = session.Flashes()
if len(flashes) != 0 {
t.Errorf("Expected empty flashes; Got %v", flashes)
}
// Add some flashes.
session.AddFlash(&FlashMessage{42, "foo"})
// Save.
if err = Save(req, rsp); err != nil {
t.Fatalf("Error saving session: %v", err)
}
hdr = rsp.Header()
cookies, ok = hdr["Set-Cookie"]
if !ok || len(cookies) != 1 {
t.Fatal("No cookies. Header:", hdr)
}
// Round 4 ----------------------------------------------------------------
// Custom type
req, _ = http.NewRequest("GET", "http://localhost:8080/", nil)
req.Header.Add("Cookie", cookies[0])
rsp = NewRecorder()
// Get a session.
if session, err = store.Get(req, "session-key"); err != nil {
t.Fatalf("Error getting session: %v", err)
}
// Check all saved values.
flashes = session.Flashes()
if len(flashes) != 1 {
t.Fatalf("Expected flashes; Got %v", flashes)
}
custom := flashes[0].(FlashMessage)
if custom.Type != 42 || custom.Message != "foo" {
t.Errorf("Expected %#v, got %#v", FlashMessage{42, "foo"}, custom)
}
}
func init() {
gob.Register(FlashMessage{})
}

View file

@ -14,7 +14,6 @@ import (
"github.com/gorilla/securecookie"
"github.com/webx-top/echo"
"github.com/webx-top/echo/engine"
)
// Store is an interface for custom session stores.
@ -28,10 +27,10 @@ type Store interface {
//
// Note that New should never return a nil session, even in the case of
// an error if using the Registry infrastructure to cache the session.
New(r engine.Request, name string) (*Session, error)
New(ctx echo.Context, name string) (*Session, error)
// Save should persist session to the underlying store implementation.
Save(r engine.Request, w engine.Response, s *Session) error
Save(ctx echo.Context, s *Session) error
}
// CookieStore ----------------------------------------------------------------
@ -86,13 +85,13 @@ func (s *CookieStore) Get(ctx echo.Context, name string) (*Session, error) {
// The difference between New() and Get() is that calling New() twice will
// decode the session data twice, while Get() registers and reuses the same
// decoded session after the first call.
func (s *CookieStore) New(r engine.Request, name string) (*Session, error) {
func (s *CookieStore) New(ctx echo.Context, name string) (*Session, error) {
session := NewSession(s, name)
opts := *s.Options
session.Options = &opts
session.IsNew = true
var err error
if v := r.Cookie(name); v != `` {
if v := ctx.Request().Cookie(name); len(v) > 0 {
err = securecookie.DecodeMulti(name, v, &session.Values,
s.Codecs...)
if err == nil {
@ -103,14 +102,13 @@ func (s *CookieStore) New(r engine.Request, name string) (*Session, error) {
}
// Save adds a single session to the response.
func (s *CookieStore) Save(r engine.Request, w engine.Response,
session *Session) error {
func (s *CookieStore) Save(ctx echo.Context, session *Session) error {
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
s.Codecs...)
if err != nil {
return err
}
w.SetCookie(NewCookie(session.Name(), encoded, session.Options))
SetCookie(ctx, session.Name(), encoded, session.Options)
return nil
}
@ -187,13 +185,13 @@ func (s *FilesystemStore) Get(ctx echo.Context, name string) (*Session, error) {
// New returns a session for the given name without adding it to the registry.
//
// See CookieStore.New().
func (s *FilesystemStore) New(r engine.Request, name string) (*Session, error) {
func (s *FilesystemStore) New(ctx echo.Context, name string) (*Session, error) {
session := NewSession(s, name)
opts := *s.Options
session.Options = &opts
session.IsNew = true
var err error
if v := r.Cookie(name); v != `` {
if v := ctx.Request().Cookie(name); len(v) > 0 {
err = securecookie.DecodeMulti(name, v, &session.ID, s.Codecs...)
if err == nil {
err = s.load(session)
@ -206,9 +204,9 @@ func (s *FilesystemStore) New(r engine.Request, name string) (*Session, error) {
}
// Save adds a single session to the response.
func (s *FilesystemStore) Save(r engine.Request, w engine.Response,
func (s *FilesystemStore) Save(ctx echo.Context,
session *Session) error {
if session.ID == "" {
if len(session.ID) == 0 {
// Because the ID is used in the filename, encode it to
// use alphanumeric characters only.
session.ID = strings.TrimRight(
@ -223,7 +221,7 @@ func (s *FilesystemStore) Save(r engine.Request, w engine.Response,
if err != nil {
return err
}
w.SetCookie(NewCookie(session.Name(), encoded, session.Options))
SetCookie(ctx, session.Name(), encoded, session.Options)
return nil
}

View file

@ -1,73 +0,0 @@
package sessions
import (
"encoding/base64"
"net/http"
"net/http/httptest"
"testing"
)
// Test for GH-8 for CookieStore
func TestGH8CookieStore(t *testing.T) {
originalPath := "/"
store := NewCookieStore()
store.Options.Path = originalPath
req, err := http.NewRequest("GET", "http://www.example.com", nil)
if err != nil {
t.Fatal("failed to create request", err)
}
session, err := store.New(req, "hello")
if err != nil {
t.Fatal("failed to create session", err)
}
store.Options.Path = "/foo"
if session.Options.Path != originalPath {
t.Fatalf("bad session path: got %q, want %q", session.Options.Path, originalPath)
}
}
// Test for GH-8 for FilesystemStore
func TestGH8FilesystemStore(t *testing.T) {
originalPath := "/"
store := NewFilesystemStore("")
store.Options.Path = originalPath
req, err := http.NewRequest("GET", "http://www.example.com", nil)
if err != nil {
t.Fatal("failed to create request", err)
}
session, err := store.New(req, "hello")
if err != nil {
t.Fatal("failed to create session", err)
}
store.Options.Path = "/foo"
if session.Options.Path != originalPath {
t.Fatalf("bad session path: got %q, want %q", session.Options.Path, originalPath)
}
}
// Test for GH-2.
func TestGH2MaxLength(t *testing.T) {
store := NewFilesystemStore("", []byte("some key"))
req, err := http.NewRequest("GET", "http://www.example.com", nil)
if err != nil {
t.Fatal("failed to create request", err)
}
w := httptest.NewRecorder()
session, err := store.New(req, "my session")
session.Values["big"] = make([]byte, base64.StdEncoding.DecodedLen(4096*2))
err = session.Save(req, w)
if err == nil {
t.Fatal("expected an error, got nil")
}
store.MaxLength(4096 * 3) // A bit more than the value size to account for encoding overhead.
err = session.Save(req, w)
if err != nil {
t.Fatal("failed to Save:", err)
}
}