Initial files.
This commit is contained in:
parent
ea6d91b8bc
commit
918ff72419
5 changed files with 819 additions and 0 deletions
27
LICENSE
Normal file
27
LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2012 Rodrigo Moraes. 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 Google Inc. 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
|
||||||
|
"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.
|
126
doc.go
Normal file
126
doc.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// 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 gorilla/sessions provides cookie and filesystem sessions and
|
||||||
|
infrastructure for custom session backends.
|
||||||
|
|
||||||
|
The key features are:
|
||||||
|
|
||||||
|
* Simple API: use it as an easy way to set signed (and optionally
|
||||||
|
encrypted) cookies.
|
||||||
|
* Built-in backends to store sessions in cookies or the filesystem.
|
||||||
|
* Flash messages: session values that last until read.
|
||||||
|
* Convenient way to switch session persistency (aka "remember me") and set
|
||||||
|
other attributes.
|
||||||
|
* Mechanism to rotate authentication and encryption keys.
|
||||||
|
* Multiple sessions per request, even using different backends.
|
||||||
|
* Interfaces and infrastructure for custom session backends: sessions from
|
||||||
|
different stores can be retrieved and batch-saved using a common API.
|
||||||
|
|
||||||
|
Let's start with an example that shows the sessions API in a nutshell:
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var store = sessions.NewCookieStore([]byte("something-very-secret"))
|
||||||
|
|
||||||
|
func MyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Get a session. We're ignoring the error resulted from decoding an
|
||||||
|
// existing session: Get() always returns a session, even if empty.
|
||||||
|
session, _ := store.Get(r, "session-name")
|
||||||
|
// Set some session values.
|
||||||
|
session.Values["foo"] = "bar"
|
||||||
|
session.Values[42] = 43
|
||||||
|
// Save it.
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
First we initialize a session store calling NewCookieStore() and passing a
|
||||||
|
secret key used to authenticate the session. Inside the handler, we call
|
||||||
|
store.Get() to retrieve an existing session or a new one. Then we set some
|
||||||
|
session values in session.Values, which is a map[interface{}]interface{}.
|
||||||
|
And finally we call session.Save() to save the session in the response.
|
||||||
|
|
||||||
|
That's all you need to know for the basic usage. Let's take a look at other
|
||||||
|
options, starting with flash messages.
|
||||||
|
|
||||||
|
Flash messages are session values that last until read. The term appeared with
|
||||||
|
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) {
|
||||||
|
// Get a session.
|
||||||
|
session, _ := store.Get(r, "session-name")
|
||||||
|
// Get the previously flashes, if any.
|
||||||
|
if flashes := session.Flashes(); len(flashes) > 0 {
|
||||||
|
// Just print the flash values.
|
||||||
|
fmt.Fprint(w, "%v", flashes)
|
||||||
|
} else {
|
||||||
|
// Set a new flash.
|
||||||
|
session.AddFlash("Hello, flash messages world!")
|
||||||
|
fmt.Fprint(w, "No flashes found.")
|
||||||
|
}
|
||||||
|
session.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
Flash messages are useful to set information to be read after a redirection,
|
||||||
|
like after form submissions.
|
||||||
|
|
||||||
|
By default, session cookies last for a month. This is probably too long for
|
||||||
|
some cases, but it is easy to change this and other attributes during
|
||||||
|
runtime. Sessions can be configured individually or the store can be
|
||||||
|
configured and then all sessions saved using it will use that configuration.
|
||||||
|
We access session.Options or store.Options to set a new configuration. The
|
||||||
|
fields are basically a subset of http.Cookie fields. Let's change the
|
||||||
|
maximum age of a session to one week:
|
||||||
|
|
||||||
|
session.Options = &sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 7,
|
||||||
|
}
|
||||||
|
|
||||||
|
Sometimes we may want to change authentication and/or encryption keys without
|
||||||
|
breaking existing sessions. The CookieStore supports key rotation, and to use
|
||||||
|
it you just need to set multiple authentication and encryption keys, in pairs,
|
||||||
|
to be tested in order:
|
||||||
|
|
||||||
|
var store = sessions.NewCookieStore(
|
||||||
|
[]byte("new-authentication-key"),
|
||||||
|
[]byte("new-encryption-key"),
|
||||||
|
[]byte("old-authentication-key"),
|
||||||
|
[]byte("old-encryption-key"),
|
||||||
|
)
|
||||||
|
|
||||||
|
New sessions will be saved using the first pair. Old sessions can still be
|
||||||
|
read because the first pair will fail, and the second will be tested. This
|
||||||
|
makes it easy to "rotate" secret keys and still be able to validate existing
|
||||||
|
sessions. Note: for all pairs the encryption key is optional; set it to nil
|
||||||
|
or omit it and and encryption won't be used.
|
||||||
|
|
||||||
|
Multiple sessions can be used in the same request, even with different
|
||||||
|
session backends. When this happens, calling Save() on each session
|
||||||
|
individually would be cumbersome, so we have a way to save all sessions
|
||||||
|
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) {
|
||||||
|
// Get a session and set a value.
|
||||||
|
session1, _ := store.Get(r, "session-one")
|
||||||
|
session1.Values["foo"] = "bar"
|
||||||
|
// Get another session and set another value.
|
||||||
|
session2, _ := store.Get(r, "session-two")
|
||||||
|
session2.Values[42] = 43
|
||||||
|
// Save all sessions.
|
||||||
|
sessions.Save(r, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
This is possible because when we call Get() from a session store, it adds the
|
||||||
|
session to a common registry. Save() uses it to save all registered sessions.
|
||||||
|
*/
|
||||||
|
package sessions
|
234
sessions.go
Normal file
234
sessions.go
Normal file
|
@ -0,0 +1,234 @@
|
||||||
|
// 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 (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default flashes key.
|
||||||
|
const flashesKey = "_flash"
|
||||||
|
|
||||||
|
// Options --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Options stores configuration for a session or session store.
|
||||||
|
//
|
||||||
|
// Fields are a subset of http.Cookie fields.
|
||||||
|
type Options struct {
|
||||||
|
Path string
|
||||||
|
Domain string
|
||||||
|
// 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.
|
||||||
|
MaxAge int
|
||||||
|
Secure bool
|
||||||
|
HttpOnly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session --------------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewSession is called by session stores to create a new session instance.
|
||||||
|
func NewSession(store Store, name string) *Session {
|
||||||
|
return &Session{
|
||||||
|
Values: make(map[interface{}]interface{}),
|
||||||
|
store: store,
|
||||||
|
name: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session stores the values and optional configuration for a session.
|
||||||
|
type Session struct {
|
||||||
|
ID string
|
||||||
|
Values map[interface{}]interface{}
|
||||||
|
Options *Options
|
||||||
|
IsNew bool
|
||||||
|
store Store
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flashes returns a slice of flash messages from the session.
|
||||||
|
//
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines
|
||||||
|
// the flash key. If not defined "_flash" is used by default.
|
||||||
|
func (s *Session) Flashes(vars ...string) []interface{} {
|
||||||
|
var flashes []interface{}
|
||||||
|
key := flashesKey
|
||||||
|
if len(vars) > 0 {
|
||||||
|
key = vars[0]
|
||||||
|
}
|
||||||
|
if v, ok := s.Values[key]; ok {
|
||||||
|
// Drop the flashes and return it.
|
||||||
|
delete(s.Values, key)
|
||||||
|
flashes = v.([]interface{})
|
||||||
|
}
|
||||||
|
return flashes
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFlash adds a flash message to the session.
|
||||||
|
//
|
||||||
|
// A single variadic argument is accepted, and it is optional: it defines
|
||||||
|
// the flash key. If not defined "_flash" is used by default.
|
||||||
|
func (s *Session) AddFlash(value interface{}, vars ...string) {
|
||||||
|
key := flashesKey
|
||||||
|
if len(vars) > 0 {
|
||||||
|
key = vars[0]
|
||||||
|
}
|
||||||
|
var flashes []interface{}
|
||||||
|
if v, ok := s.Values[key]; ok {
|
||||||
|
flashes = v.([]interface{})
|
||||||
|
}
|
||||||
|
s.Values[key] = append(flashes, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save is a convenience method to save this session. It is the same as calling
|
||||||
|
// store.Save(request, response, session)
|
||||||
|
func (s *Session) Save(r *http.Request, w http.ResponseWriter) error {
|
||||||
|
return s.store.Save(r, w, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name used to register the session.
|
||||||
|
func (s *Session) Name() string {
|
||||||
|
return s.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store returns the session store used to register the session.
|
||||||
|
func (s *Session) Store() Store {
|
||||||
|
return s.store
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry -------------------------------------------------------------------
|
||||||
|
|
||||||
|
// sessionInfo stores a session tracked by the registry.
|
||||||
|
type sessionInfo struct {
|
||||||
|
s *Session
|
||||||
|
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
|
||||||
|
|
||||||
|
// GetRegistry returns a registry instance for the current request.
|
||||||
|
func GetRegistry(r *http.Request) *Registry {
|
||||||
|
registry := context.Get(r, registryKey)
|
||||||
|
if registry != nil {
|
||||||
|
return registry.(*Registry)
|
||||||
|
}
|
||||||
|
newRegistry := &Registry{
|
||||||
|
request: r,
|
||||||
|
sessions: make(map[string]sessionInfo),
|
||||||
|
}
|
||||||
|
context.Set(r, registryKey, newRegistry)
|
||||||
|
return newRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registry stores sessions used during a request.
|
||||||
|
type Registry struct {
|
||||||
|
request *http.Request
|
||||||
|
sessions map[string]sessionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get registers and returns a session for the given name and session store.
|
||||||
|
//
|
||||||
|
// It returns a new session if there are no sessions registered for the name.
|
||||||
|
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.name = name
|
||||||
|
s.sessions[name] = sessionInfo{s: session, e: err}
|
||||||
|
}
|
||||||
|
session.store = store
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves all sessions registered for the current request.
|
||||||
|
func (s *Registry) Save(w http.ResponseWriter) 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 {
|
||||||
|
errMulti = append(errMulti, fmt.Errorf(
|
||||||
|
"sessions: error saving session %q -- %v", name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if errMulti != nil {
|
||||||
|
return errMulti
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers --------------------------------------------------------------------
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register([]interface{}{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves all sessions used during the current request.
|
||||||
|
func Save(r *http.Request, w http.ResponseWriter) error {
|
||||||
|
return GetRegistry(r).Save(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCookie returns an http.Cookie with the options set. It also sets
|
||||||
|
// the Expires field calculated based on the MaxAge value, for Internet
|
||||||
|
// Explorer compatibility.
|
||||||
|
func NewCookie(name, value string, options *Options) *http.Cookie {
|
||||||
|
cookie := &http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Path: options.Path,
|
||||||
|
Domain: options.Domain,
|
||||||
|
MaxAge: options.MaxAge,
|
||||||
|
Secure: options.Secure,
|
||||||
|
HttpOnly: options.HttpOnly,
|
||||||
|
}
|
||||||
|
if options.MaxAge > 0 {
|
||||||
|
d := time.Duration(options.MaxAge) * time.Second
|
||||||
|
cookie.Expires = time.Now().Add(d)
|
||||||
|
} else if options.MaxAge < 0 {
|
||||||
|
// Set it to the past to expire now.
|
||||||
|
cookie.Expires = time.Unix(1, 0)
|
||||||
|
}
|
||||||
|
return cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
// MultiError stores multiple errors.
|
||||||
|
//
|
||||||
|
// Borrowed from the App Engine SDK.
|
||||||
|
type MultiError []error
|
||||||
|
|
||||||
|
func (m MultiError) Error() string {
|
||||||
|
s, n := "", 0
|
||||||
|
for _, e := range m {
|
||||||
|
if e != nil {
|
||||||
|
if n == 0 {
|
||||||
|
s = e.Error()
|
||||||
|
}
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
return "(0 errors)"
|
||||||
|
case 1:
|
||||||
|
return s
|
||||||
|
case 2:
|
||||||
|
return s + " (and 1 other error)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (and %d other errors)", s, n-1)
|
||||||
|
}
|
197
sessions_test.go
Normal file
197
sessions_test.go
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
// 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.Fatalf("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.Fatalf("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{})
|
||||||
|
}
|
235
store.go
Normal file
235
store.go
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
// 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 (
|
||||||
|
"encoding/base32"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store is an interface for custom session stores.
|
||||||
|
type Store interface {
|
||||||
|
Get(r *http.Request, name string) (*Session, error)
|
||||||
|
New(r *http.Request, name string) (*Session, error)
|
||||||
|
Save(r *http.Request, w http.ResponseWriter, s *Session) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieStore ----------------------------------------------------------------
|
||||||
|
|
||||||
|
// NewCookieStore returns a new CookieStore.
|
||||||
|
//
|
||||||
|
// Keys are defined in pairs to allow key rotation, but the common case is
|
||||||
|
// to set a single authentication key and optionally an encryption key.
|
||||||
|
//
|
||||||
|
// The first key in a pair is used for authentication and the second for
|
||||||
|
// encryption. The encryption key can be set to nil or omitted in the last
|
||||||
|
// pair, but the authentication key is required in all pairs.
|
||||||
|
//
|
||||||
|
// It is recommended to use an authentication key with 32 or 64 bytes.
|
||||||
|
// The encryption key, if set, must be either 16, 24, or 32 bytes to select
|
||||||
|
// AES-128, AES-192, or AES-256 modes.
|
||||||
|
//
|
||||||
|
// Use the convenience function securecookie.GenerateRandomKey() to create
|
||||||
|
// strong keys.
|
||||||
|
func NewCookieStore(keyPairs ...[]byte) *CookieStore {
|
||||||
|
return &CookieStore{
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||||
|
Options: &Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 30,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieStore stores sessions using secure cookies.
|
||||||
|
type CookieStore struct {
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *Options // default configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// It returns a new session if the sessions doesn't exist. Access IsNew on
|
||||||
|
// the session to check if it is an existing session or a new one.
|
||||||
|
//
|
||||||
|
// It returns a new session and an error if the session exists but could
|
||||||
|
// not be decoded.
|
||||||
|
func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) {
|
||||||
|
return GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// 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 *http.Request, name string) (*Session, error) {
|
||||||
|
session := NewSession(s, name)
|
||||||
|
session.Options = &(*s.Options)
|
||||||
|
session.IsNew = true
|
||||||
|
var err error
|
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter,
|
||||||
|
session *Session) error {
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemStore ------------------------------------------------------------
|
||||||
|
|
||||||
|
var fileMutex sync.RWMutex
|
||||||
|
|
||||||
|
// NewFilesystemStore returns a new FilesystemStore.
|
||||||
|
//
|
||||||
|
// The path argument is the directory where sessions will be saved. If empty
|
||||||
|
// it will use os.TempDir().
|
||||||
|
//
|
||||||
|
// See NewCookieStore() for a description of the other parameters.
|
||||||
|
func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore {
|
||||||
|
if path == "" {
|
||||||
|
path = os.TempDir()
|
||||||
|
}
|
||||||
|
if path[len(path)-1] != '/' {
|
||||||
|
path += "/"
|
||||||
|
}
|
||||||
|
return &FilesystemStore{
|
||||||
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
||||||
|
Options: &Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: 86400 * 30,
|
||||||
|
},
|
||||||
|
path: path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilesystemStore stores sessions in the filesystem.
|
||||||
|
//
|
||||||
|
// It also serves as a referece for custom stores.
|
||||||
|
//
|
||||||
|
// This store is still experimental and not well tested. Feedback is welcome.
|
||||||
|
type FilesystemStore struct {
|
||||||
|
Codecs []securecookie.Codec
|
||||||
|
Options *Options // default configuration
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns a session for the given name after adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.Get().
|
||||||
|
func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) {
|
||||||
|
return GetRegistry(r).Get(s, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a session for the given name without adding it to the registry.
|
||||||
|
//
|
||||||
|
// See CookieStore.New().
|
||||||
|
func (s *FilesystemStore) New(r *http.Request, name string) (*Session, error) {
|
||||||
|
session := NewSession(s, name)
|
||||||
|
session.Options = &(*s.Options)
|
||||||
|
session.IsNew = true
|
||||||
|
var err error
|
||||||
|
if c, errCookie := r.Cookie(name); errCookie == nil {
|
||||||
|
err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...)
|
||||||
|
if err == nil {
|
||||||
|
err = s.load(session)
|
||||||
|
if err == nil {
|
||||||
|
session.IsNew = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save adds a single session to the response.
|
||||||
|
func (s *FilesystemStore) Save(r *http.Request, w http.ResponseWriter,
|
||||||
|
session *Session) error {
|
||||||
|
if session.ID == "" {
|
||||||
|
// Because the ID is used in the filename, encode it to
|
||||||
|
// use alphanumeric characters only.
|
||||||
|
session.ID = strings.TrimRight(
|
||||||
|
base32.StdEncoding.EncodeToString(
|
||||||
|
securecookie.GenerateRandomKey(32)), "=")
|
||||||
|
}
|
||||||
|
if err := s.save(session); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.ID,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
http.SetCookie(w, NewCookie(session.Name(), encoded, session.Options))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save writes encoded session.Values to a file.
|
||||||
|
func (s *FilesystemStore) save(session *Session) error {
|
||||||
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values,
|
||||||
|
s.Codecs...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filename := s.path + "session_" + session.ID
|
||||||
|
fileMutex.Lock()
|
||||||
|
defer fileMutex.Unlock()
|
||||||
|
fp, err2 := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600)
|
||||||
|
if err2 != nil {
|
||||||
|
return err2
|
||||||
|
}
|
||||||
|
if _, err = fp.Write([]byte(encoded)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fp.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// load reads a file and decodes its content into session.Values.
|
||||||
|
func (s *FilesystemStore) load(session *Session) error {
|
||||||
|
filename := s.path + "session_" + session.ID
|
||||||
|
fp, err := os.OpenFile(filename, os.O_RDONLY, 0400)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
var fdata []byte
|
||||||
|
buf := make([]byte, 128)
|
||||||
|
for {
|
||||||
|
var n int
|
||||||
|
n, err = fp.Read(buf[0:])
|
||||||
|
fdata = append(fdata, buf[0:n]...)
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err = securecookie.DecodeMulti(session.Name(), string(fdata),
|
||||||
|
&session.Values, s.Codecs...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Reference in a new issue