├── LICENSE ├── README.md ├── benchmarks_test.go ├── cookie_store.go ├── redis_store.go ├── sessions.go ├── sessions_test.go └── wercker.yml /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jeremy Saenz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sessions [![wercker status](https://app.wercker.com/status/af92c7633124fffea8984e48ee0c418b "wercker status")](https://app.wercker.com/project/bykey/af92c7633124fffea8984e48ee0c418b) 2 | Martini middleware/handler for easy session management. 3 | 4 | [API Reference](http://godoc.org/github.com/martini-contrib/sessions) 5 | 6 | ## Usage 7 | 8 | ~~~ go 9 | package main 10 | 11 | import ( 12 | "github.com/go-martini/martini" 13 | "github.com/martini-contrib/sessions" 14 | ) 15 | 16 | func main() { 17 | m := martini.Classic() 18 | 19 | store := sessions.NewCookieStore([]byte("secret123")) 20 | m.Use(sessions.Sessions("my_session", store)) 21 | 22 | m.Get("/set", func(session sessions.Session) string { 23 | session.Set("hello", "world") 24 | return "OK" 25 | }) 26 | 27 | m.Get("/get", func(session sessions.Session) string { 28 | v := session.Get("hello") 29 | if v == nil { 30 | return "" 31 | } 32 | return v.(string) 33 | }) 34 | 35 | m.Run() 36 | } 37 | 38 | ~~~ 39 | 40 | ## Authors 41 | * [Jeremy Saenz](http://github.com/codegangsta) 42 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "github.com/go-martini/martini" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkNoSessionsMiddleware(b *testing.B) { 11 | m := testMartini() 12 | m.Get("/foo", func() string { 13 | return "Foo" 14 | }) 15 | 16 | recorder := httptest.NewRecorder() 17 | r, _ := http.NewRequest("GET", "/foo", nil) 18 | 19 | b.ResetTimer() 20 | for n := 0; n < b.N; n++ { 21 | m.ServeHTTP(recorder, r) 22 | } 23 | } 24 | 25 | func BenchmarkSessionsNoWrites(b *testing.B) { 26 | m := testMartini() 27 | store := NewCookieStore([]byte("secret123")) 28 | m.Use(Sessions("my_session", store)) 29 | m.Get("/foo", func() string { 30 | return "Foo" 31 | }) 32 | 33 | recorder := httptest.NewRecorder() 34 | r, _ := http.NewRequest("GET", "/foo", nil) 35 | 36 | b.ResetTimer() 37 | for n := 0; n < b.N; n++ { 38 | m.ServeHTTP(recorder, r) 39 | } 40 | } 41 | 42 | func BenchmarkSessionsWithWrite(b *testing.B) { 43 | m := testMartini() 44 | store := NewCookieStore([]byte("secret123")) 45 | m.Use(Sessions("my_session", store)) 46 | m.Get("/foo", func(s Session) string { 47 | s.Set("foo", "bar") 48 | return "Foo" 49 | }) 50 | 51 | recorder := httptest.NewRecorder() 52 | r, _ := http.NewRequest("GET", "/foo", nil) 53 | 54 | b.ResetTimer() 55 | for n := 0; n < b.N; n++ { 56 | m.ServeHTTP(recorder, r) 57 | } 58 | } 59 | 60 | func BenchmarkSessionsWithRead(b *testing.B) { 61 | m := testMartini() 62 | store := NewCookieStore([]byte("secret123")) 63 | m.Use(Sessions("my_session", store)) 64 | m.Get("/foo", func(s Session) string { 65 | s.Get("foo") 66 | return "Foo" 67 | }) 68 | 69 | recorder := httptest.NewRecorder() 70 | r, _ := http.NewRequest("GET", "/foo", nil) 71 | 72 | b.ResetTimer() 73 | for n := 0; n < b.N; n++ { 74 | m.ServeHTTP(recorder, r) 75 | } 76 | } 77 | 78 | func testMartini() *martini.ClassicMartini { 79 | m := martini.Classic() 80 | m.Handlers() 81 | return m 82 | } 83 | -------------------------------------------------------------------------------- /cookie_store.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "github.com/gorilla/sessions" 5 | ) 6 | 7 | // CookieStore is an interface that represents a Cookie based storage 8 | // for Sessions. 9 | type CookieStore interface { 10 | // Store is an embedded interface so that CookieStore can be used 11 | // as a session store. 12 | Store 13 | // Options sets the default options for each session stored in this 14 | // CookieStore. 15 | Options(Options) 16 | } 17 | 18 | // NewCookieStore returns a new CookieStore. 19 | // 20 | // Keys are defined in pairs to allow key rotation, but the common case is to set a single 21 | // authentication key and optionally an encryption key. 22 | // 23 | // The first key in a pair is used for authentication and the second for encryption. The 24 | // encryption key can be set to nil or omitted in the last pair, but the authentication key 25 | // is required in all pairs. 26 | // 27 | // It is recommended to use an authentication key with 32 or 64 bytes. The encryption key, 28 | // if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes. 29 | func NewCookieStore(keyPairs ...[]byte) CookieStore { 30 | return &cookieStore{sessions.NewCookieStore(keyPairs...)} 31 | } 32 | 33 | type cookieStore struct { 34 | *sessions.CookieStore 35 | } 36 | 37 | func (c *cookieStore) Options(options Options) { 38 | c.CookieStore.Options = &sessions.Options{ 39 | Path: options.Path, 40 | Domain: options.Domain, 41 | MaxAge: options.MaxAge, 42 | Secure: options.Secure, 43 | HttpOnly: options.HttpOnly, 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /redis_store.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "github.com/boj/redistore" 5 | "github.com/gorilla/sessions" 6 | ) 7 | 8 | // RedisStore is an interface that represents a Cookie based storage 9 | // for Sessions. 10 | type RediStore interface { 11 | // Store is an embedded interface so that RedisStore can be used 12 | // as a session store. 13 | Store 14 | // Options sets the default options for each session stored in this 15 | // CookieStore. 16 | Options(Options) 17 | } 18 | 19 | // NewCookieStore returns a new CookieStore. 20 | // 21 | // Keys are defined in pairs to allow key rotation, but the common case is to set a single 22 | // authentication key and optionally an encryption key. 23 | // 24 | // The first key in a pair is used for authentication and the second for encryption. The 25 | // encryption key can be set to nil or omitted in the last pair, but the authentication key 26 | // is required in all pairs. 27 | // 28 | // It is recommended to use an authentication key with 32 or 64 bytes. The encryption key, 29 | // if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes. 30 | func NewRediStore(size int, network, address, password string, keyPairs ...[]byte) (RediStore, error) { 31 | store, err := redistore.NewRediStore(size, network, address, password, keyPairs...) 32 | if err != nil { 33 | return nil, err 34 | } 35 | return &rediStore{store}, nil 36 | } 37 | 38 | type rediStore struct { 39 | *redistore.RediStore 40 | } 41 | 42 | func (c *rediStore) Options(options Options) { 43 | c.RediStore.Options = &sessions.Options{ 44 | Path: options.Path, 45 | Domain: options.Domain, 46 | MaxAge: options.MaxAge, 47 | Secure: options.Secure, 48 | HttpOnly: options.HttpOnly, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sessions.go: -------------------------------------------------------------------------------- 1 | // Package sessions contains middleware for easy session management in Martini. 2 | // 3 | // package main 4 | // 5 | // import ( 6 | // "github.com/go-martini/martini" 7 | // "github.com/martini-contrib/sessions" 8 | // ) 9 | // 10 | // func main() { 11 | // m := martini.Classic() 12 | // 13 | // store := sessions.NewCookieStore([]byte("secret123")) 14 | // m.Use(sessions.Sessions("my_session", store)) 15 | // 16 | // m.Get("/", func(session sessions.Session) string { 17 | // session.Set("hello", "world") 18 | // }) 19 | // } 20 | package sessions 21 | 22 | import ( 23 | "github.com/go-martini/martini" 24 | "github.com/gorilla/context" 25 | "github.com/gorilla/sessions" 26 | "log" 27 | "net/http" 28 | ) 29 | 30 | const ( 31 | errorFormat = "[sessions] ERROR! %s\n" 32 | ) 33 | 34 | // Store is an interface for custom session stores. 35 | type Store interface { 36 | sessions.Store 37 | } 38 | 39 | // Options stores configuration for a session or session store. 40 | // 41 | // Fields are a subset of http.Cookie fields. 42 | type Options struct { 43 | Path string 44 | Domain string 45 | // MaxAge=0 means no 'Max-Age' attribute specified. 46 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'. 47 | // MaxAge>0 means Max-Age attribute present and given in seconds. 48 | MaxAge int 49 | Secure bool 50 | HttpOnly bool 51 | } 52 | 53 | // Session stores the values and optional configuration for a session. 54 | type Session interface { 55 | // Get returns the session value associated to the given key. 56 | Get(key interface{}) interface{} 57 | // Set sets the session value associated to the given key. 58 | Set(key interface{}, val interface{}) 59 | // Delete removes the session value associated to the given key. 60 | Delete(key interface{}) 61 | // Clear deletes all values in the session. 62 | Clear() 63 | // AddFlash adds a flash message to the session. 64 | // A single variadic argument is accepted, and it is optional: it defines the flash key. 65 | // If not defined "_flash" is used by default. 66 | AddFlash(value interface{}, vars ...string) 67 | // Flashes returns a slice of flash messages from the session. 68 | // A single variadic argument is accepted, and it is optional: it defines the flash key. 69 | // If not defined "_flash" is used by default. 70 | Flashes(vars ...string) []interface{} 71 | // Options sets confuguration for a session. 72 | Options(Options) 73 | } 74 | 75 | // Sessions is a Middleware that maps a session.Session service into the Martini handler chain. 76 | // Sessions can use a number of storage solutions with the given store. 77 | func Sessions(name string, store Store) martini.Handler { 78 | return func(res http.ResponseWriter, r *http.Request, c martini.Context, l *log.Logger) { 79 | // Map to the Session interface 80 | s := &session{name, r, l, store, nil, false} 81 | c.MapTo(s, (*Session)(nil)) 82 | 83 | // Use before hook to save out the session 84 | rw := res.(martini.ResponseWriter) 85 | rw.Before(func(martini.ResponseWriter) { 86 | if s.Written() { 87 | check(s.Session().Save(r, res), l) 88 | } 89 | }) 90 | 91 | // clear the context, we don't need to use 92 | // gorilla context and we don't want memory leaks 93 | defer context.Clear(r) 94 | 95 | c.Next() 96 | } 97 | } 98 | 99 | type session struct { 100 | name string 101 | request *http.Request 102 | logger *log.Logger 103 | store Store 104 | session *sessions.Session 105 | written bool 106 | } 107 | 108 | func (s *session) Get(key interface{}) interface{} { 109 | return s.Session().Values[key] 110 | } 111 | 112 | func (s *session) Set(key interface{}, val interface{}) { 113 | s.Session().Values[key] = val 114 | s.written = true 115 | } 116 | 117 | func (s *session) Delete(key interface{}) { 118 | delete(s.Session().Values, key) 119 | s.written = true 120 | } 121 | 122 | func (s *session) Clear() { 123 | for key := range s.Session().Values { 124 | s.Delete(key) 125 | } 126 | } 127 | 128 | func (s *session) AddFlash(value interface{}, vars ...string) { 129 | s.Session().AddFlash(value, vars...) 130 | s.written = true 131 | } 132 | 133 | func (s *session) Flashes(vars ...string) []interface{} { 134 | s.written = true 135 | return s.Session().Flashes(vars...) 136 | } 137 | 138 | func (s *session) Options(options Options) { 139 | s.Session().Options = &sessions.Options{ 140 | Path: options.Path, 141 | Domain: options.Domain, 142 | MaxAge: options.MaxAge, 143 | Secure: options.Secure, 144 | HttpOnly: options.HttpOnly, 145 | } 146 | } 147 | 148 | func (s *session) Session() *sessions.Session { 149 | if s.session == nil { 150 | var err error 151 | s.session, err = s.store.Get(s.request, s.name) 152 | check(err, s.logger) 153 | } 154 | 155 | return s.session 156 | } 157 | 158 | func (s *session) Written() bool { 159 | return s.written 160 | } 161 | 162 | func check(err error, l *log.Logger) { 163 | if err != nil { 164 | l.Printf(errorFormat, err) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /sessions_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "github.com/go-martini/martini" 5 | "net/http" 6 | "net/http/httptest" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func Test_Sessions(t *testing.T) { 12 | m := martini.Classic() 13 | 14 | store := NewCookieStore([]byte("secret123")) 15 | m.Use(Sessions("my_session", store)) 16 | 17 | m.Get("/testsession", func(session Session) string { 18 | session.Set("hello", "world") 19 | return "OK" 20 | }) 21 | 22 | m.Get("/show", func(session Session) string { 23 | if session.Get("hello") != "world" { 24 | t.Error("Session writing failed") 25 | } 26 | return "OK" 27 | }) 28 | 29 | res := httptest.NewRecorder() 30 | req, _ := http.NewRequest("GET", "/testsession", nil) 31 | m.ServeHTTP(res, req) 32 | 33 | res2 := httptest.NewRecorder() 34 | req2, _ := http.NewRequest("GET", "/show", nil) 35 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 36 | m.ServeHTTP(res2, req2) 37 | } 38 | 39 | func Test_SessionsDeleteValue(t *testing.T) { 40 | m := martini.Classic() 41 | 42 | store := NewCookieStore([]byte("secret123")) 43 | m.Use(Sessions("my_session", store)) 44 | 45 | m.Get("/testsession", func(session Session) string { 46 | session.Set("hello", "world") 47 | session.Delete("hello") 48 | return "OK" 49 | }) 50 | 51 | m.Get("/show", func(session Session) string { 52 | if session.Get("hello") == "world" { 53 | t.Error("Session value deleting failed") 54 | } 55 | return "OK" 56 | }) 57 | 58 | res := httptest.NewRecorder() 59 | req, _ := http.NewRequest("GET", "/testsession", nil) 60 | m.ServeHTTP(res, req) 61 | 62 | res2 := httptest.NewRecorder() 63 | req2, _ := http.NewRequest("GET", "/show", nil) 64 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 65 | m.ServeHTTP(res2, req2) 66 | } 67 | 68 | func Test_Options(t *testing.T) { 69 | m := martini.Classic() 70 | store := NewCookieStore([]byte("secret123")) 71 | store.Options(Options{ 72 | Domain: "martini.codegangsta.io", 73 | }) 74 | m.Use(Sessions("my_session", store)) 75 | 76 | m.Get("/", func(session Session) string { 77 | session.Set("hello", "world") 78 | session.Options(Options{ 79 | Path: "/foo/bar/bat", 80 | }) 81 | return "OK" 82 | }) 83 | 84 | m.Get("/foo", func(session Session) string { 85 | session.Set("hello", "world") 86 | return "OK" 87 | }) 88 | 89 | res := httptest.NewRecorder() 90 | req, _ := http.NewRequest("GET", "/", nil) 91 | m.ServeHTTP(res, req) 92 | 93 | res2 := httptest.NewRecorder() 94 | req2, _ := http.NewRequest("GET", "/foo", nil) 95 | m.ServeHTTP(res2, req2) 96 | 97 | s := strings.Split(res.Header().Get("Set-Cookie"), ";") 98 | if s[1] != " Path=/foo/bar/bat" { 99 | t.Error("Error writing path with options:", s[1]) 100 | } 101 | 102 | s = strings.Split(res2.Header().Get("Set-Cookie"), ";") 103 | if s[1] != " Domain=martini.codegangsta.io" { 104 | t.Error("Error writing domain with options:", s[1]) 105 | } 106 | } 107 | 108 | func Test_Flashes(t *testing.T) { 109 | m := martini.Classic() 110 | 111 | store := NewCookieStore([]byte("secret123")) 112 | m.Use(Sessions("my_session", store)) 113 | 114 | m.Get("/set", func(session Session) string { 115 | session.AddFlash("hello world") 116 | return "OK" 117 | }) 118 | 119 | m.Get("/show", func(session Session) string { 120 | l := len(session.Flashes()) 121 | if l != 1 { 122 | t.Error("Flashes count does not equal 1. Equals ", l) 123 | } 124 | return "OK" 125 | }) 126 | 127 | m.Get("/showagain", func(session Session) string { 128 | l := len(session.Flashes()) 129 | if l != 0 { 130 | t.Error("flashes count is not 0 after reading. Equals ", l) 131 | } 132 | return "OK" 133 | }) 134 | 135 | res := httptest.NewRecorder() 136 | req, _ := http.NewRequest("GET", "/set", nil) 137 | m.ServeHTTP(res, req) 138 | 139 | res2 := httptest.NewRecorder() 140 | req2, _ := http.NewRequest("GET", "/show", nil) 141 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 142 | m.ServeHTTP(res2, req2) 143 | 144 | res3 := httptest.NewRecorder() 145 | req3, _ := http.NewRequest("GET", "/showagain", nil) 146 | req3.Header.Set("Cookie", res2.Header().Get("Set-Cookie")) 147 | m.ServeHTTP(res3, req3) 148 | } 149 | 150 | func Test_SessionsClear(t *testing.T) { 151 | m := martini.Classic() 152 | data := map[string]string{ 153 | "hello": "world", 154 | "foo": "bar", 155 | "apples": "oranges", 156 | } 157 | 158 | store := NewCookieStore([]byte("secret123")) 159 | m.Use(Sessions("my_session", store)) 160 | 161 | m.Get("/testsession", func(session Session) string { 162 | for k, v := range data { 163 | session.Set(k, v) 164 | } 165 | session.Clear() 166 | return "OK" 167 | }) 168 | 169 | m.Get("/show", func(session Session) string { 170 | for k, v := range data { 171 | if session.Get(k) == v { 172 | t.Fatal("Session clear failed") 173 | } 174 | } 175 | return "OK" 176 | }) 177 | 178 | res := httptest.NewRecorder() 179 | req, _ := http.NewRequest("GET", "/testsession", nil) 180 | m.ServeHTTP(res, req) 181 | 182 | res2 := httptest.NewRecorder() 183 | req2, _ := http.NewRequest("GET", "/show", nil) 184 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 185 | m.ServeHTTP(res2, req2) 186 | } 187 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang@1.1.1 --------------------------------------------------------------------------------