From 01e1477aabbf51b8897fce7d9d1673389163b264 Mon Sep 17 00:00:00 2001 From: JT Olds Date: Mon, 6 Feb 2017 14:39:21 -0700 Subject: [PATCH] appengine: delayed cookie store initialization --- gensym_test.go | 1 - whauth/auth.go | 4 +-- whsess/cookie.go | 86 ++++++++++++++++++++++++++++++++++++++++-------- whsess/store.go | 18 +++++----- 4 files changed, 84 insertions(+), 25 deletions(-) diff --git a/gensym_test.go b/gensym_test.go index 5e2c464..c813509 100644 --- a/gensym_test.go +++ b/gensym_test.go @@ -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" ) diff --git a/whauth/auth.go b/whauth/auth.go index 7180b7b..1e7eb82 100644 --- a/whauth/auth.go +++ b/whauth/auth.go @@ -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() @@ -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")) diff --git a/whsess/cookie.go b/whsess/cookie.go index e3cff28..72fe97a 100644 --- a/whsess/cookie.go +++ b/whsess/cookie.go @@ -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 { @@ -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) @@ -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 @@ -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 } @@ -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 } @@ -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, @@ -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: "", diff --git a/whsess/store.go b/whsess/store.go index 6fad721..c69319e 100644 --- a/whsess/store.go +++ b/whsess/store.go @@ -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 { @@ -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 } @@ -89,8 +91,8 @@ 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 } @@ -98,10 +100,10 @@ func (s *Session) Save(w http.ResponseWriter) error { } // 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) }