Initial files.

This commit is contained in:
moraes 2012-10-03 01:49:24 -03:00
parent ea6d91b8bc
commit 918ff72419
5 changed files with 819 additions and 0 deletions

27
LICENSE Normal file
View 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
View 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
View 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
View 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
View 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
}