diff --git a/doc.go b/doc.go index 946bf5c..2f1cb30 100644 --- a/doc.go +++ b/doc.go @@ -203,5 +203,29 @@ at once: it's sessions.Save(). Here's an example: 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. + +Sometimes it may happen that an HTTP request includes two cookies of the same +name. This will usually happen when `sessions.Save` is called twice. To handle +these situations gracefully, you can use the `GetExact` method to find an exact +match for your cookie. Please note that the store must implement the StoreExact +interface for this to work. All default store implementations in this repository +implement StoreExact. + + 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.GetExact(r, "session-one", func(s *sessions.Session) { + // Only reuse cookies that match "foo" value of "baz". + return s.Values["foo"] == "baz" + }) + session1.Values["foo"] = "bar" + // Save sessions. + err = sessions.Save(r, w) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } */ package sessions diff --git a/go.mod b/go.mod index 9028bcf..094896f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ -module github.com/gorilla/sessions +module github.com/gunsluo/sessions + +go 1.16 require github.com/gorilla/securecookie v1.1.1 diff --git a/matcher.go b/matcher.go new file mode 100644 index 0000000..5696e1b --- /dev/null +++ b/matcher.go @@ -0,0 +1,10 @@ +package sessions + +// Matcher defines a function used to identify the correct session and cookie +// for a request which contains multiple session cookies of the same name. +type Matcher func(session *Session) bool + +// FirstMatcher returns instructs to use the first session and cookie of a given request +func FirstMatcher(_ *Session) bool { + return true +} diff --git a/matcher_test.go b/matcher_test.go new file mode 100644 index 0000000..b9e6a5f --- /dev/null +++ b/matcher_test.go @@ -0,0 +1,9 @@ +package sessions + +import "testing" + +func TestFirstMatcher(t *testing.T) { + if !FirstMatcher(nil) { + t.Error("First matcher should match first session") + } +} diff --git a/sessions.go b/sessions.go index c052b28..3ed9b76 100644 --- a/sessions.go +++ b/sessions.go @@ -129,17 +129,42 @@ type Registry struct { // 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. +// +// If more than one cookie with the given name is found, only the one cookie +// is used to find the session. func (s *Registry) Get(store Store, name string) (session *Session, err error) { + return s.GetExact(store, name, FirstMatcher) +} + +// GetExact registers and returns a session for the given name and session store. +// +// It returns a new session if there are no sessions found. Please note that +// the underlying store must implement the StoreExact interface or using this +// method will always return the first cookie found and ignore the matcher +// parameter. +// +// Use the matcher function to explicitly set which existing session to use +// if multiple cookies with the same name have been sent. +// +// Unless this method returns an error, it will always return at least one session +// in the slice. +func (s *Registry) GetExact(store Store, name string, matcher Matcher) (session *Session, err error) { if !isCookieNameValid(name) { return nil, fmt.Errorf("sessions: invalid character in cookie name: %s", name) } - if info, ok := s.sessions[name]; ok { - session, err = info.s, info.e - } else { + + if info, ok := s.sessions[name]; ok && matcher(info.s) { + return info.s, info.e + } + + switch ts := store.(type) { + case StoreExact: + session, err = ts.NewExact(s.request, name, matcher) + default: session, err = store.New(s.request, name) - session.name = name - s.sessions[name] = sessionInfo{s: session, e: err} } + session.name = name + s.sessions[name] = sessionInfo{s: session, e: err} session.store = store return } diff --git a/sessions_test.go b/sessions_test.go index 0b84fbc..9458bc7 100644 --- a/sessions_test.go +++ b/sessions_test.go @@ -182,6 +182,60 @@ func TestFlashes(t *testing.T) { } } +func TestUsesExactStore(t *testing.T) { + store := NewCookieStore([]byte("aaa0defe5d2839cbc46fc4f080cd7adc")) + + req, err := http.NewRequest("GET", "http://www.example.com", nil) + if err != nil { + t.Fatal("failed to create request", err) + } + + w := httptest.NewRecorder() + session := NewSession(store, "hello") + session.Values["data"] = "foo" + err = session.Save(req, w) + if err != nil { + t.Fatal("failed to save session", err) + } + + session = NewSession(store, "hello") + session.Values["data"] = "bar" + session.Options.Domain = "www.example.com" + err = session.Save(req, w) + if err != nil { + t.Fatal("failed to save session", err) + } + + req, err = http.NewRequest("GET", "http://www.example.com", nil) + if err != nil { + t.Fatal("failed to create request", err) + } + + if len(w.Result().Cookies()) != 2 { + t.Fatalf("expected two cookies but got 0: %+v", w.Result().Header) + } + + for _, cookie := range w.Result().Cookies() { + req.AddCookie(cookie) + } + + for _, expected := range []string{ + "bar", + // Doing it again returns the accurate cookie as well, even though we are using the cache now. + "foo", + } { + actual, err := store.GetExact(req, "hello", func(s *Session) bool { + return s.Values["data"] == expected + }) + if err != nil { + t.Fatal("failed to get cookie request", err) + } + if actual.Values["data"] != expected { + t.Fatalf("expected session data to be '%s' but got %v", expected, actual.Values) + } + } +} + func TestCookieStoreMapPanic(t *testing.T) { defer func() { err := recover() diff --git a/store.go b/store.go index eb45afb..4f66533 100644 --- a/store.go +++ b/store.go @@ -32,6 +32,32 @@ type Store interface { Save(r *http.Request, w http.ResponseWriter, s *Session) error } +// StoreExact is an interface for custom session stores with matching capabilities. +// +// Stores should implement this interface if they want the consumer to be able +// to decide which exact session store to use when multiple sessions are given. +// +// See CookieStore and FilesystemStore for examples. +type StoreExact interface { + Store + + // NewExact should return a session for the given name and matcher.go + // without adding it to the registry. + // + // The difference between StoreExact.NewExact() and Store.New() is that + // calling Store.New() will find one cookie using the given name. If + // multiple cookies with the same name exist, it will return only one of + // them. Calling StoreExact.NewExact() allows you to further specify + // which cookie when multiple cookies with the same name exist. + // + // Please note that only the first value for which matcher.go returns true will be used. + NewExact(r *http.Request, name string, matcher Matcher) (*Session, error) + + // GetExact should return a cached session and uses StoreExact.NewExact, with + // the same rules applied, instead of Store.New to find the appropriate cookie. + GetExact(r *http.Request, name string, matcher Matcher) (*Session, error) +} + // CookieStore ---------------------------------------------------------------- // NewCookieStore returns a new CookieStore. @@ -65,6 +91,10 @@ type CookieStore struct { Options *Options // default configuration } +// Type guards +var _ Store = (*CookieStore)(nil) +var _ StoreExact = (*CookieStore)(nil) + // 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 @@ -76,6 +106,12 @@ func (s *CookieStore) Get(r *http.Request, name string) (*Session, error) { return GetRegistry(r).Get(s, name) } +// GetExact returns a cached session and uses StoreExact.NewExact, with +// the same rules applied, instead of Store.New to find the appropriate cookie. +func (s *CookieStore) GetExact(r *http.Request, name string, matcher Matcher) (*Session, error) { + return GetRegistry(r).GetExact(s, name, matcher) +} + // 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 @@ -97,6 +133,39 @@ func (s *CookieStore) New(r *http.Request, name string) (*Session, error) { return session, err } +// NewExact returns a session for the given name and matcher.go without adding it to the registry. +// +// The difference between NewExact() and New() is that calling New() will find one +// cookie using the given name. If multiple cookies with the same name exist, it will +// return only one of them. Calling NewMatch() allows you to further specify which cookie +// when multiple cookies with the same name exist. +// +// Please note that only the first value for which matcher.go returns true will be used. +func (s *CookieStore) NewExact(r *http.Request, name string, matcher Matcher) (*Session, error) { + session := NewSession(s, name) + opts := *s.Options + session.Options = &opts + session.IsNew = true + var err error + for _, c := range r.Cookies() { + if c.Name != name { + continue + } + + err = securecookie.DecodeMulti(name, c.Value, &session.Values, + s.Codecs...) + if err == nil { + if matcher(session) { + session.IsNew = false + break + } + session.Values = make(map[interface{}]interface{}) + } + } + + return session, err +} + // Save adds a single session to the response. func (s *CookieStore) Save(r *http.Request, w http.ResponseWriter, session *Session) error { @@ -161,6 +230,10 @@ type FilesystemStore struct { path string } +// Type guards +var _ Store = (*FilesystemStore)(nil) +var _ StoreExact = (*FilesystemStore)(nil) + // MaxLength restricts the maximum length of new sessions to l. // If l is 0 there is no limit to the size of a session, use with caution. // The default for a new FilesystemStore is 4096. @@ -179,24 +252,50 @@ func (s *FilesystemStore) Get(r *http.Request, name string) (*Session, error) { return GetRegistry(r).Get(s, name) } +// GetExact returns a cached session and uses StoreExact.NewExact, with +// the same rules applied, instead of Store.New to find the appropriate cookie. +func (s *FilesystemStore) GetExact(r *http.Request, name string, matcher Matcher) (*Session, error) { + return GetRegistry(r).GetExact(s, name, matcher) +} + // 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) { + return s.NewExact(r, name, FirstMatcher) +} + +// NewExact returns a session for the given name and matcher.go without adding it to the registry. +// +// The difference between NewExact() and New() is that calling New() will find one +// cookie using the given name. If multiple cookies with the same name exist, it will +// return only one of them. Calling NewMatch() allows you to further specify which cookie +// when multiple cookies with the same name exist. +// +// Please note that only the first value for which matcher.go returns true will be used. +func (s *FilesystemStore) NewExact(r *http.Request, name string, matcher Matcher) (*Session, error) { session := NewSession(s, name) opts := *s.Options session.Options = &opts session.IsNew = true var err error - if c, errCookie := r.Cookie(name); errCookie == nil { + for _, c := range r.Cookies() { + if c.Name != name { + continue + } + + session.Values = make(map[interface{}]interface{}) err = securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) if err == nil { err = s.load(session) if err == nil { - session.IsNew = false + if matcher(session) { + session.IsNew = false + } } } } + return session, err }