Skip to content

Commit

Permalink
appengine: delayed cookie store initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
jtolio committed Feb 6, 2017
1 parent 9334fa3 commit 01e1477
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 25 deletions.
1 change: 0 additions & 1 deletion gensym_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"gopkg.in/webhelp.v1/whcompat"
"gopkg.in/webhelp.v1/wherr"
"gopkg.in/webhelp.v1/whlog"
"gopkg.in/webhelp.v1/whmux"
"gopkg.in/webhelp.v1/whroute"
)

Expand Down
4 changes: 2 additions & 2 deletions whauth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ var (
// RequireBasicAuth ensures that a valid user is provided, calling
// wherr.Handle with wherr.Unauthorized if not.
func RequireBasicAuth(h http.Handler, realm string,
valid func(user, pass string) bool) http.Handler {
valid func(ctx context.Context, user, pass string) bool) http.Handler {
return whroute.HandlerFunc(h,
func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
Expand All @@ -31,7 +31,7 @@ func RequireBasicAuth(h http.Handler, realm string,
wherr.Handle(w, r, wherr.Unauthorized.New("basic auth required"))
return
}
if !valid(user, pass) {
if !valid(whcompat.Context(r), user, pass) {
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
wherr.Handle(w, r,
wherr.Unauthorized.New("invalid username or password"))
Expand Down
86 changes: 72 additions & 14 deletions whsess/cookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@ package whsess
import (
"bytes"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/gob"
"net/http"
"sync"

"golang.org/x/crypto/nacl/secretbox"
"golang.org/x/net/context"
"gopkg.in/webhelp.v1/whcompat"
"gopkg.in/webhelp.v1/wherr"
)

const (
nonceLength = 24
KeyLength = 32
nonceLength = 24
keyLength = 32
minKeyLength = 10
)

type CookieOptions struct {
Expand All @@ -27,8 +33,12 @@ type CookieOptions struct {
}

type CookieStore struct {
Options CookieOptions
Secret [KeyLength]byte
Options CookieOptions
secretCB func(context.Context) ([]byte, error)

secretMtx sync.Mutex
secretSetup bool
secret [keyLength]byte
}

var _ Store = (*CookieStore)(nil)
Expand All @@ -39,15 +49,57 @@ func NewCookieStore(secretKey []byte) *CookieStore {
rv := &CookieStore{
Options: CookieOptions{
Path: "/",
MaxAge: 86400 * 30}}
copy(rv.Secret[:], secretKey)
MaxAge: 86400 * 30},
secretCB: func(context.Context) ([]byte, error) {
return secretKey, nil
},
}
return rv
}

// NewLazyCookieStore is like NewCookieStore but loads the secretKey using
// the provided callback once. This is useful for delayed initialization after
// the first request for something like App Engine where you can't interact
// with a database without a context.
func NewLazyCookieStore(secretKey func(context.Context) ([]byte, error)) (
cs *CookieStore) {
rv := &CookieStore{
Options: CookieOptions{
Path: "/",
MaxAge: 86400 * 30},
secretCB: secretKey,
}
return rv
}

func (cs *CookieStore) getSecret(ctx context.Context) (
*[keyLength]byte, error) {
cs.secretMtx.Lock()
defer cs.secretMtx.Unlock()
if cs.secretSetup {
return &cs.secret, nil
}
secret, err := cs.secretCB(ctx)
if err != nil {
return nil, err
}
if len(secret) < minKeyLength {
return nil, wherr.InternalServerError.New("cookie secret not long enough")
}
secretHash := sha256.Sum256(secret)
copy(cs.secret[:], secretHash[:])
cs.secretSetup = true
return &cs.secret, nil
}

// Load implements the Store interface. Not expected to be used directly.
func (cs *CookieStore) Load(r *http.Request, namespace string) (rv SessionData,
err error) {
func (cs *CookieStore) Load(ctx context.Context, r *http.Request,
namespace string) (rv SessionData, err error) {
empty := SessionData{New: true, Values: map[interface{}]interface{}{}}
secret, err := cs.getSecret(whcompat.Context(r))
if err != nil {
return empty, err
}
c, err := r.Cookie(namespace)
if err != nil || c.Value == "" {
return empty, nil
Expand All @@ -59,7 +111,7 @@ func (cs *CookieStore) Load(r *http.Request, namespace string) (rv SessionData,
var nonce [nonceLength]byte
copy(nonce[:], data[:nonceLength])
decrypted, ok := secretbox.Open(nil, data[nonceLength:], &nonce,
&cs.Secret)
secret)
if !ok {
return empty, nil
}
Expand All @@ -71,10 +123,15 @@ func (cs *CookieStore) Load(r *http.Request, namespace string) (rv SessionData,
}

// Save implements the Store interface. Not expected to be used directly.
func (cs *CookieStore) Save(w http.ResponseWriter, namespace string,
s SessionData) error {
func (cs *CookieStore) Save(ctx context.Context, w http.ResponseWriter,
namespace string, s SessionData) error {
secret, err := cs.getSecret(ctx)
if err != nil {
return err
}

var out bytes.Buffer
err := gob.NewEncoder(&out).Encode(&s.Values)
err = gob.NewEncoder(&out).Encode(&s.Values)
if err != nil {
return err
}
Expand All @@ -84,7 +141,7 @@ func (cs *CookieStore) Save(w http.ResponseWriter, namespace string,
return err
}
value := base64.URLEncoding.EncodeToString(
secretbox.Seal(nonce[:], out.Bytes(), &nonce, &cs.Secret))
secretbox.Seal(nonce[:], out.Bytes(), &nonce, secret))

return setCookie(w, &http.Cookie{
Name: namespace,
Expand All @@ -106,7 +163,8 @@ func setCookie(w http.ResponseWriter, cookie *http.Cookie) error {
}

// Clear implements the Store interface. Not expected to be used directly.
func (cs *CookieStore) Clear(w http.ResponseWriter, namespace string) error {
func (cs *CookieStore) Clear(ctx context.Context, w http.ResponseWriter,
namespace string) error {
return setCookie(w, &http.Cookie{
Name: namespace,
Value: "",
Expand Down
18 changes: 10 additions & 8 deletions whsess/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ type Session struct {
}

type Store interface {
Load(r *http.Request, namespace string) (SessionData, error)
Save(w http.ResponseWriter, namespace string, s SessionData) error
Clear(w http.ResponseWriter, namespace string) error
Load(ctx context.Context, r *http.Request, namespace string) (
SessionData, error)
Save(ctx context.Context, w http.ResponseWriter,
namespace string, s SessionData) error
Clear(ctx context.Context, w http.ResponseWriter, namespace string) error
}

type reqCtx struct {
Expand Down Expand Up @@ -72,7 +74,7 @@ func Load(ctx context.Context, namespace string) (*Session, error) {
return session, nil
}
}
sessiondata, err := rc.s.Load(rc.r, namespace)
sessiondata, err := rc.s.Load(ctx, rc.r, namespace)
if err != nil {
return nil, err
}
Expand All @@ -89,19 +91,19 @@ func Load(ctx context.Context, namespace string) (*Session, error) {
}

// Save saves the session using the appropriate mechanism.
func (s *Session) Save(w http.ResponseWriter) error {
err := s.store.Save(w, s.namespace, s.SessionData)
func (s *Session) Save(ctx context.Context, w http.ResponseWriter) error {
err := s.store.Save(ctx, w, s.namespace, s.SessionData)
if err == nil {
s.SessionData.New = false
}
return err
}

// Clear clears the session using the appropriate mechanism.
func (s *Session) Clear(w http.ResponseWriter) error {
func (s *Session) Clear(ctx context.Context, w http.ResponseWriter) error {
// clear out the cache
for name := range s.Values {
delete(s.Values, name)
}
return s.store.Clear(w, s.namespace)
return s.store.Clear(ctx, w, s.namespace)
}

0 comments on commit 01e1477

Please sign in to comment.