├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cache.go ├── cache_test.go ├── examples └── server.go ├── memstore.go └── memstore_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - tip 5 | before_install: 6 | - go get github.com/mattn/goveralls 7 | script: 8 | - $GOPATH/bin/goveralls -service=travis-ci -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2018, QuaSoft 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memstore 2 | 3 | [![GoDoc](https://godoc.org/github.com/quasoft/memstore?status.svg)](https://godoc.org/github.com/quasoft/memstore) [![Build Status](https://travis-ci.org/quasoft/memstore.png?branch=master)](https://travis-ci.org/quasoft/memstore) [![Coverage Status](https://coveralls.io/repos/github/quasoft/memstore/badge.svg?branch=master)](https://coveralls.io/github/quasoft/memstore?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/quasoft/memstore)](https://goreportcard.com/report/github.com/quasoft/memstore) 4 | 5 | In-memory implementation of [gorilla/sessions](https://github.com/gorilla/sessions) for use in tests and dev environments 6 | 7 | ## How to install 8 | 9 | go get github.com/quasoft/memstore 10 | 11 | ## Documentation 12 | 13 | Documentation, as usual, can be found at [godoc.org](http://www.godoc.org/github.com/quasoft/memstore). 14 | 15 | The interface of [gorilla/sessions](https://github.com/gorilla/sessions) is described at http://www.gorillatoolkit.org/pkg/sessions. 16 | 17 | ### How to use 18 | ``` go 19 | package main 20 | 21 | import ( 22 | "fmt" 23 | "log" 24 | "net/http" 25 | 26 | "github.com/quasoft/memstore" 27 | ) 28 | 29 | func main() { 30 | // Create a memory store, providing authentication and 31 | // encryption key for securecookie 32 | store := memstore.NewMemStore( 33 | []byte("authkey123"), 34 | []byte("enckey12341234567890123456789012"), 35 | ) 36 | 37 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 38 | // Get session by name. 39 | session, err := store.Get(r, "session1") 40 | if err != nil { 41 | log.Printf("Error retrieving session: %v", err) 42 | } 43 | 44 | // The name should be 'foobar' if home page was visited before that and 'Guest' otherwise. 45 | user, ok := session.Values["username"] 46 | if !ok { 47 | user = "Guest" 48 | } 49 | fmt.Fprintf(w, "Hello %s", user) 50 | }) 51 | 52 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 53 | // Get session by name. 54 | session, err := store.Get(r, "session1") 55 | if err != nil { 56 | log.Printf("Error retrieving session: %v", err) 57 | } 58 | 59 | // Add values to the session object 60 | session.Values["username"] = "foobar" 61 | session.Values["email"] = "spam@eggs.com" 62 | 63 | // Save values 64 | err = session.Save(r, w) 65 | if err != nil { 66 | log.Fatalf("Error saving session: %v", err) 67 | } 68 | }) 69 | 70 | log.Printf("listening on http://%s/", "127.0.0.1:9090") 71 | log.Fatal(http.ListenAndServe("127.0.0.1:9090", nil)) 72 | } 73 | 74 | ``` 75 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package memstore 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type cache struct { 8 | data map[string]valueType 9 | mutex sync.RWMutex 10 | } 11 | 12 | func newCache() *cache { 13 | return &cache{ 14 | data: make(map[string]valueType), 15 | } 16 | } 17 | 18 | func (c *cache) value(name string) (valueType, bool) { 19 | c.mutex.RLock() 20 | defer c.mutex.RUnlock() 21 | 22 | v, ok := c.data[name] 23 | return v, ok 24 | } 25 | 26 | func (c *cache) setValue(name string, value valueType) { 27 | c.mutex.Lock() 28 | defer c.mutex.Unlock() 29 | 30 | c.data[name] = value 31 | } 32 | 33 | func (c *cache) delete(name string) { 34 | c.mutex.Lock() 35 | defer c.mutex.Unlock() 36 | 37 | if _, ok := c.data[name]; ok { 38 | delete(c.data, name) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | package memstore 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func Test_newCache(t *testing.T) { 9 | cache := newCache() 10 | if cache == nil { 11 | t.Error("newCache() got nil") 12 | } 13 | } 14 | 15 | func Test_cache_value(t *testing.T) { 16 | cache := newCache() 17 | cache.setValue("key1", valueType{"subkey1": "value1"}) 18 | cache.setValue("key2", nil) 19 | cache.setValue("key3", valueType{"subkey3": nil}) 20 | 21 | tests := []struct { 22 | name string 23 | key string 24 | want valueType 25 | wantOk bool 26 | }{ 27 | {"Existing key", "key1", valueType{"subkey1": "value1"}, true}, 28 | {"Existing key with nil value type", "key2", nil, true}, 29 | {"Existing key and subkey with nil value", "key3", valueType{"subkey3": nil}, true}, 30 | {"Not existing key", "thereisnokey", nil, false}, 31 | } 32 | 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | got, gotOk := cache.value(tt.key) 36 | 37 | if gotOk != tt.wantOk { 38 | t.Errorf("cache.value(%v) got ok = %v, want %v", tt.key, gotOk, tt.wantOk) 39 | } 40 | 41 | if gotOk && !reflect.DeepEqual(got, tt.want) { 42 | t.Errorf("cache.value(%v) got = %v, want %v", tt.key, got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func Test_cache_delete(t *testing.T) { 49 | cache := newCache() 50 | 51 | cache.setValue("key1", valueType{"subkey1": "value1"}) 52 | 53 | _, gotOk := cache.value("key1") 54 | if !gotOk { 55 | t.Error(`cache.value("key1") got ok = false, want true`) 56 | } 57 | 58 | cache.delete("key1") 59 | 60 | _, gotOk = cache.value("key1") 61 | if gotOk { 62 | t.Error(`cache.value("key1") got ok = true, want false`) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/quasoft/memstore" 9 | ) 10 | 11 | func main() { 12 | // Create a memory store, providing authentication and 13 | // encryption key for securecookie 14 | store := memstore.NewMemStore( 15 | []byte("authkey123"), 16 | []byte("enckey12341234567890123456789012"), 17 | ) 18 | 19 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 20 | // Get session by name. 21 | session, err := store.Get(r, "session1") 22 | if err != nil { 23 | log.Printf("Error retrieving session: %v", err) 24 | } 25 | 26 | // The name should be 'foobar' if home page was visited before that and 'Guest' otherwise. 27 | user, ok := session.Values["username"] 28 | if !ok { 29 | user = "Guest" 30 | } 31 | fmt.Fprintf(w, "Hello %s", user) 32 | }) 33 | 34 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 35 | // Get session by name. 36 | session, err := store.Get(r, "session1") 37 | if err != nil { 38 | log.Printf("Error retrieving session: %v", err) 39 | } 40 | 41 | // Add values to the session object 42 | session.Values["username"] = "foobar" 43 | session.Values["email"] = "spam@eggs.com" 44 | 45 | // Save values 46 | err = session.Save(r, w) 47 | if err != nil { 48 | log.Fatalf("Error saving session: %v", err) 49 | } 50 | }) 51 | 52 | log.Printf("listening on http://%s/", "127.0.0.1:9090") 53 | log.Fatal(http.ListenAndServe("127.0.0.1:9090", nil)) 54 | } 55 | -------------------------------------------------------------------------------- /memstore.go: -------------------------------------------------------------------------------- 1 | package memstore 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base32" 6 | "encoding/gob" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/gorilla/securecookie" 12 | "github.com/gorilla/sessions" 13 | ) 14 | 15 | // MemStore is an in-memory implementation of gorilla/sessions, suitable 16 | // for use in tests and development environments. Do not use in production. 17 | // Values are cached in a map. The cache is protected and can be used by 18 | // multiple goroutines. 19 | type MemStore struct { 20 | Codecs []securecookie.Codec 21 | Options *sessions.Options 22 | cache *cache 23 | } 24 | 25 | type valueType map[interface{}]interface{} 26 | 27 | // NewMemStore returns a new MemStore. 28 | // 29 | // Keys are defined in pairs to allow key rotation, but the common case is 30 | // to set a single authentication key and optionally an encryption key. 31 | // 32 | // The first key in a pair is used for authentication and the second for 33 | // encryption. The encryption key can be set to nil or omitted in the last 34 | // pair, but the authentication key is required in all pairs. 35 | // 36 | // It is recommended to use an authentication key with 32 or 64 bytes. 37 | // The encryption key, if set, must be either 16, 24, or 32 bytes to select 38 | // AES-128, AES-192, or AES-256 modes. 39 | // 40 | // Use the convenience function securecookie.GenerateRandomKey() to create 41 | // strong keys. 42 | func NewMemStore(keyPairs ...[]byte) *MemStore { 43 | store := MemStore{ 44 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 45 | Options: &sessions.Options{ 46 | Path: "/", 47 | MaxAge: 86400 * 30, 48 | }, 49 | cache: newCache(), 50 | } 51 | store.MaxAge(store.Options.MaxAge) 52 | return &store 53 | } 54 | 55 | // Get returns a session for the given name after adding it to the registry. 56 | // 57 | // It returns a new session if the sessions doesn't exist. Access IsNew on 58 | // the session to check if it is an existing session or a new one. 59 | // 60 | // It returns a new session and an error if the session exists but could 61 | // not be decoded. 62 | func (m *MemStore) Get(r *http.Request, name string) (*sessions.Session, error) { 63 | return sessions.GetRegistry(r).Get(m, name) 64 | } 65 | 66 | // New returns a session for the given name without adding it to the registry. 67 | // 68 | // The difference between New() and Get() is that calling New() twice will 69 | // decode the session data twice, while Get() registers and reuses the same 70 | // decoded session after the first call. 71 | func (m *MemStore) New(r *http.Request, name string) (*sessions.Session, error) { 72 | session := sessions.NewSession(m, name) 73 | options := *m.Options 74 | session.Options = &options 75 | session.IsNew = true 76 | 77 | c, err := r.Cookie(name) 78 | if err != nil { 79 | // Cookie not found, this is a new session 80 | return session, nil 81 | } 82 | 83 | err = securecookie.DecodeMulti(name, c.Value, &session.ID, m.Codecs...) 84 | if err != nil { 85 | // Value could not be decrypted, consider this is a new session 86 | return session, err 87 | } 88 | 89 | v, ok := m.cache.value(session.ID) 90 | if !ok { 91 | // No value found in cache, don't set any values in session object, 92 | // consider a new session 93 | return session, nil 94 | } 95 | 96 | // Values found in session, this is not a new session 97 | session.Values = m.copy(v) 98 | session.IsNew = false 99 | return session, nil 100 | } 101 | 102 | // Save adds a single session to the response. 103 | // Set Options.MaxAge to -1 or call MaxAge(-1) before saving the session to delete all values in it. 104 | func (m *MemStore) Save(r *http.Request, w http.ResponseWriter, s *sessions.Session) error { 105 | var cookieValue string 106 | if s.Options.MaxAge < 0 { 107 | cookieValue = "" 108 | m.cache.delete(s.ID) 109 | for k := range s.Values { 110 | delete(s.Values, k) 111 | } 112 | } else { 113 | if s.ID == "" { 114 | s.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=") 115 | } 116 | encrypted, err := securecookie.EncodeMulti(s.Name(), s.ID, m.Codecs...) 117 | if err != nil { 118 | return err 119 | } 120 | cookieValue = encrypted 121 | m.cache.setValue(s.ID, m.copy(s.Values)) 122 | } 123 | http.SetCookie(w, sessions.NewCookie(s.Name(), cookieValue, s.Options)) 124 | return nil 125 | } 126 | 127 | // MaxAge sets the maximum age for the store and the underlying cookie 128 | // implementation. Individual sessions can be deleted by setting Options.MaxAge 129 | // = -1 for that session. 130 | func (m *MemStore) MaxAge(age int) { 131 | m.Options.MaxAge = age 132 | 133 | // Set the maxAge for each securecookie instance. 134 | for _, codec := range m.Codecs { 135 | if sc, ok := codec.(*securecookie.SecureCookie); ok { 136 | sc.MaxAge(age) 137 | } 138 | } 139 | } 140 | 141 | func (m *MemStore) copy(v valueType) valueType { 142 | var buf bytes.Buffer 143 | enc := gob.NewEncoder(&buf) 144 | dec := gob.NewDecoder(&buf) 145 | err := enc.Encode(v) 146 | if err != nil { 147 | panic(fmt.Errorf("could not copy memstore value. Encoding to gob failed: %v", err)) 148 | } 149 | var value valueType 150 | err = dec.Decode(&value) 151 | if err != nil { 152 | panic(fmt.Errorf("could not copy memstore value. Decoding from gob failed: %v", err)) 153 | } 154 | return value 155 | } 156 | -------------------------------------------------------------------------------- /memstore_test.go: -------------------------------------------------------------------------------- 1 | package memstore 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/gorilla/sessions" 11 | ) 12 | 13 | func TestMemStore_Get(t *testing.T) { 14 | store := NewMemStore( 15 | []byte("authkey"), 16 | []byte("enckey1234567890"), 17 | ) 18 | 19 | req := httptest.NewRequest("GET", "http://test.local", nil) 20 | _, err := store.Get(req, "mycookiename") 21 | if err != nil { 22 | t.Errorf("failed to create session from empty request: %v", err) 23 | } 24 | } 25 | 26 | func TestMemStore_Get_Bogus(t *testing.T) { 27 | store := NewMemStore( 28 | []byte("authkey"), 29 | []byte("enckey1234567890"), 30 | ) 31 | 32 | req := httptest.NewRequest("GET", "http://test.local", nil) 33 | req.AddCookie(sessions.NewCookie("mycookiename", "SomeBogusValueThatIsActuallyNotEncrypted", store.Options)) 34 | _, err := store.Get(req, "mycookiename") 35 | if err == nil { 36 | t.Error(`store.Get(req, "mycookiename") should have returned error if cookie value is bogus`) 37 | } 38 | } 39 | 40 | func TestMemStore_New(t *testing.T) { 41 | store := NewMemStore( 42 | []byte("authkey"), 43 | []byte("enckey1234567890"), 44 | ) 45 | 46 | req := httptest.NewRequest("GET", "http://test.local", nil) 47 | _, err := store.New(req, "mycookiename") 48 | if err != nil { 49 | t.Errorf("failed to create session from empty request: %v", err) 50 | } 51 | } 52 | 53 | func TestMemStore_Save(t *testing.T) { 54 | store := NewMemStore( 55 | []byte("authkey"), 56 | []byte("enckey1234567890"), 57 | ) 58 | 59 | want := "value123" 60 | req := httptest.NewRequest("GET", "http://test.local", nil) 61 | rec := httptest.NewRecorder() 62 | session, err := store.Get(req, "mycookiename") 63 | if err != nil { 64 | t.Fatalf("failed to create session from empty request: %v", err) 65 | } 66 | session.Values["key"] = want 67 | session.Save(req, rec) 68 | 69 | cookie := rec.Header().Get("Set-Cookie") 70 | if !strings.Contains(cookie, "mycookiename") { 71 | t.Error("cookie was not stored in request") 72 | } 73 | } 74 | 75 | func TestMemStore_Save_Multiple_Requests(t *testing.T) { 76 | store := NewMemStore( 77 | []byte("authkey"), 78 | []byte("enckey1234567890"), 79 | ) 80 | 81 | want := "value123" 82 | 83 | req1 := httptest.NewRequest("GET", "http://test.local", nil) 84 | rec1 := httptest.NewRecorder() 85 | session1, err := store.Get(req1, "mycookiename") 86 | if err != nil { 87 | t.Fatalf("failed to create session from empty request: %v", err) 88 | } 89 | session1.Values["key"] = want 90 | session1.Save(req1, rec1) 91 | 92 | req2 := httptest.NewRequest("GET", "http://test.local", nil) 93 | // Simulate retaining cookie from previous response 94 | req2.AddCookie(rec1.Result().Cookies()[0]) 95 | session2, err := store.Get(req2, "mycookiename") 96 | if err != nil { 97 | t.Fatalf("failed to create session from second request: %v", err) 98 | } 99 | got := session2.Values["key"] 100 | if got != want { 101 | t.Errorf(`session2.Values["key"] got = %q, want %q`, got, want) 102 | } 103 | } 104 | 105 | func TestMemStore_Delete(t *testing.T) { 106 | store := NewMemStore( 107 | []byte("authkey"), 108 | []byte("enckey1234567890"), 109 | ) 110 | 111 | req := httptest.NewRequest("GET", "http://test.local", nil) 112 | rec := httptest.NewRecorder() 113 | session, err := store.Get(req, "mycookiename") 114 | if err != nil { 115 | t.Fatalf("failed to create session from empty request: %v", err) 116 | } 117 | 118 | // Save some value 119 | session.Values["key"] = "somevalue" 120 | session.Save(req, rec) 121 | 122 | // And immediately delete it 123 | session.Options.MaxAge = -1 124 | session.Save(req, rec) 125 | 126 | if session.Values["key"] == "somevalue" { 127 | t.Error("cookie was not deleted from session after setting session.Options.MaxAge = -1 and saving") 128 | } 129 | } 130 | 131 | func BenchmarkRace(b *testing.B) { 132 | store := NewMemStore( 133 | []byte("authkey"), 134 | []byte("enckey1234567890"), 135 | ) 136 | 137 | var wg sync.WaitGroup 138 | 139 | s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 140 | defer wg.Done() 141 | 142 | session, err := store.Get(r, "mycookiename") 143 | if err != nil { 144 | b.Fatalf("failed to create session from empty request: %v", err) 145 | } 146 | session.Values["key"] = "somevalue" 147 | session.Save(r, w) 148 | 149 | // And immediately delete it 150 | session.Options.MaxAge = -1 151 | session.Save(r, w) 152 | 153 | _ = session.Values["key"] 154 | })) 155 | defer s.Close() 156 | 157 | loops := 100 158 | wg.Add(loops) 159 | for i := 1; i <= loops; i++ { 160 | go http.Get(s.URL) 161 | } 162 | wg.Wait() 163 | } 164 | --------------------------------------------------------------------------------