├── .github └── workflows │ ├── bump.yml │ └── ci.yml ├── Bumpfile ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── gormstore.go ├── gormstore_test.go ├── test └── v2 ├── go.mod ├── go.sum ├── gormstore.go ├── gormstore_test.go └── test /.github/workflows/bump.yml: -------------------------------------------------------------------------------- 1 | name: 'Automatic version updates' 2 | on: 3 | schedule: 4 | # minute hour dom month dow (UTC) 5 | - cron: '0 16 * * *' 6 | # enable manual trigger of version updates 7 | workflow_dispatch: 8 | jobs: 9 | version_update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | - uses: wader/bump/action/go@master 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.BUMP_TOKEN }} 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | # enable manual trigger 9 | workflow_dispatch: 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest] 18 | go: [1.20.x, 1.19.x, 1.18.x] 19 | steps: 20 | - name: Checkout code 21 | uses: actions/checkout@v2 22 | - name: Setup Go 23 | uses: actions/setup-go@v2 24 | with: 25 | go-version: ${{ matrix.go }} 26 | - name: Test 27 | run: ./test 28 | - name: 29 | run: cd v2 && ./test 30 | -------------------------------------------------------------------------------- /Bumpfile: -------------------------------------------------------------------------------- 1 | v2/go.mod -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Mattias Wadman 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### GORM backend for gorilla sessions 2 | 3 | For gorm v2 use: 4 | ``` 5 | import "github.com/wader/gormstore/v2" 6 | ``` 7 | For gorm v1 use: 8 | ``` 9 | import "github.com/wader/gormstore" 10 | ``` 11 | 12 | #### Documentation 13 | 14 | https://pkg.go.dev/github.com/wader/gormstore?tab=doc 15 | 16 | #### Example 17 | 18 | ```go 19 | // initialize and setup cleanup 20 | store := gormstore.New(gorm.Open(...), []byte("secret")) 21 | // db cleanup every hour 22 | // close quit channel to stop cleanup 23 | quit := make(chan struct{}) 24 | go store.PeriodicCleanup(1*time.Hour, quit) 25 | ``` 26 | 27 | ```go 28 | // in HTTP handler 29 | func handlerFunc(w http.ResponseWriter, r *http.Request) { 30 | session, err := store.Get(r, "session") 31 | session.Values["user_id"] = 123 32 | store.Save(r, w, session) 33 | http.Error(w, "", http.StatusOK) 34 | } 35 | ``` 36 | 37 | For more details see [gormstore documentation](https://pkg.go.dev/github.com/wader/gormstore?tab=doc). 38 | 39 | #### Testing 40 | 41 | Just sqlite3 tests: 42 | 43 | go test 44 | 45 | All databases using docker: 46 | 47 | ./test 48 | 49 | If docker is not local (docker-machine etc): 50 | 51 | DOCKER_IP=$(docker-machine ip dev) ./test 52 | 53 | #### License 54 | 55 | gormstore is licensed under the MIT license. See [LICENSE](LICENSE) for the full license text. 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wader/gormstore 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/go-sql-driver/mysql v1.7.0 7 | github.com/gorilla/securecookie v1.1.1 8 | github.com/gorilla/sessions v1.2.1 9 | github.com/jinzhu/gorm v1.9.16 10 | github.com/jinzhu/now v1.1.2 // indirect 11 | github.com/lib/pq v1.10.7 12 | github.com/mattn/go-sqlite3 v1.14.16 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 2 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 3 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 4 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 5 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 6 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 7 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 8 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 9 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 10 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 11 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 12 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 13 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 14 | github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= 15 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 16 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 17 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 18 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 19 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 20 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 21 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 22 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 23 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 24 | github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= 25 | github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 26 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 27 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 28 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 29 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 30 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 31 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd h1:GGJVjV8waZKRHrgwvtH66z9ZGVurTD1MT0n1Bb+q4aM= 32 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 33 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 34 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 35 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 36 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 37 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 38 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 41 | -------------------------------------------------------------------------------- /gormstore.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gormstore is a GORM backend for gorilla sessions 3 | 4 | Simplest form: 5 | 6 | store := gormstore.New(gorm.Open(...), []byte("secret-hash-key")) 7 | 8 | All options: 9 | 10 | store := gormstore.NewOptions( 11 | gorm.Open(...), // *gorm.DB 12 | gormstore.Options{ 13 | TableName: "sessions", // "sessions" is default 14 | SkipCreateTable: false, // false is default 15 | }, 16 | []byte("secret-hash-key"), // 32 or 64 bytes recommended, required 17 | []byte("secret-encyption-key")) // nil, 16, 24 or 32 bytes, optional 18 | 19 | // some more settings, see sessions.Options 20 | store.SessionOpts.Secure = true 21 | store.SessionOpts.HttpOnly = true 22 | store.SessionOpts.MaxAge = 60 * 60 * 24 * 60 23 | 24 | If you want periodic cleanup of expired sessions: 25 | 26 | quit := make(chan struct{}) 27 | go store.PeriodicCleanup(1*time.Hour, quit) 28 | 29 | For more information about the keys see https://github.com/gorilla/securecookie 30 | 31 | For API to use in HTTP handlers see https://github.com/gorilla/sessions 32 | */ 33 | package gormstore 34 | 35 | import ( 36 | "encoding/base32" 37 | "net/http" 38 | "strings" 39 | "time" 40 | 41 | "github.com/gorilla/securecookie" 42 | "github.com/gorilla/sessions" 43 | "github.com/jinzhu/gorm" 44 | ) 45 | 46 | const sessionIDLen = 32 47 | const defaultTableName = "sessions" 48 | const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days 49 | const defaultPath = "/" 50 | 51 | // Options for gormstore 52 | type Options struct { 53 | TableName string 54 | SkipCreateTable bool 55 | } 56 | 57 | // Store represent a gormstore 58 | type Store struct { 59 | db *gorm.DB 60 | opts Options 61 | Codecs []securecookie.Codec 62 | SessionOpts *sessions.Options 63 | } 64 | 65 | type gormSession struct { 66 | ID string `sql:"unique_index"` 67 | Data string `sql:"type:text"` 68 | CreatedAt time.Time 69 | UpdatedAt time.Time 70 | ExpiresAt time.Time `sql:"index"` 71 | 72 | tableName string `sql:"-"` // just for convenience instead of db.Table(...) 73 | } 74 | 75 | func (gs *gormSession) TableName() string { 76 | return gs.tableName 77 | } 78 | 79 | // New creates a new gormstore session 80 | func New(db *gorm.DB, keyPairs ...[]byte) *Store { 81 | return NewOptions(db, Options{}, keyPairs...) 82 | } 83 | 84 | // NewOptions creates a new gormstore session with options 85 | func NewOptions(db *gorm.DB, opts Options, keyPairs ...[]byte) *Store { 86 | st := &Store{ 87 | db: db, 88 | opts: opts, 89 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 90 | SessionOpts: &sessions.Options{ 91 | Path: defaultPath, 92 | MaxAge: defaultMaxAge, 93 | }, 94 | } 95 | if st.opts.TableName == "" { 96 | st.opts.TableName = defaultTableName 97 | } 98 | 99 | if !st.opts.SkipCreateTable { 100 | st.db.AutoMigrate(&gormSession{tableName: st.opts.TableName}) 101 | } 102 | 103 | return st 104 | } 105 | 106 | // Get returns a session for the given name after adding it to the registry. 107 | func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) { 108 | return sessions.GetRegistry(r).Get(st, name) 109 | } 110 | 111 | // New creates a session with name without adding it to the registry. 112 | func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) { 113 | session := sessions.NewSession(st, name) 114 | opts := *st.SessionOpts 115 | session.Options = &opts 116 | session.IsNew = true 117 | 118 | st.MaxAge(st.SessionOpts.MaxAge) 119 | 120 | // try fetch from db if there is a cookie 121 | s := st.getSessionFromCookie(r, session.Name()) 122 | if s != nil { 123 | if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil { 124 | return session, nil 125 | } 126 | session.ID = s.ID 127 | session.IsNew = false 128 | } 129 | 130 | return session, nil 131 | } 132 | 133 | // Save session and set cookie header 134 | func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { 135 | s := st.getSessionFromCookie(r, session.Name()) 136 | 137 | // delete if max age is < 0 138 | if session.Options.MaxAge < 0 { 139 | if s != nil { 140 | if err := st.db.Delete(s).Error; err != nil { 141 | return err 142 | } 143 | } 144 | http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) 145 | return nil 146 | } 147 | 148 | data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...) 149 | if err != nil { 150 | return err 151 | } 152 | now := time.Now() 153 | expire := now.Add(time.Second * time.Duration(session.Options.MaxAge)) 154 | 155 | if s == nil { 156 | // generate random session ID key suitable for storage in the db 157 | session.ID = strings.TrimRight( 158 | base32.StdEncoding.EncodeToString( 159 | securecookie.GenerateRandomKey(sessionIDLen)), "=") 160 | s = &gormSession{ 161 | ID: session.ID, 162 | Data: data, 163 | CreatedAt: now, 164 | UpdatedAt: now, 165 | ExpiresAt: expire, 166 | tableName: st.opts.TableName, 167 | } 168 | if err := st.db.Create(s).Error; err != nil { 169 | return err 170 | } 171 | } else { 172 | s.Data = data 173 | s.UpdatedAt = now 174 | s.ExpiresAt = expire 175 | if err := st.db.Save(s).Error; err != nil { 176 | return err 177 | } 178 | } 179 | 180 | // set session id cookie 181 | id, err := securecookie.EncodeMulti(session.Name(), s.ID, st.Codecs...) 182 | if err != nil { 183 | return err 184 | } 185 | http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options)) 186 | 187 | return nil 188 | } 189 | 190 | // getSessionFromCookie looks for an existing gormSession from a session ID stored inside a cookie 191 | func (st *Store) getSessionFromCookie(r *http.Request, name string) *gormSession { 192 | if cookie, err := r.Cookie(name); err == nil { 193 | sessionID := "" 194 | if err := securecookie.DecodeMulti(name, cookie.Value, &sessionID, st.Codecs...); err != nil { 195 | return nil 196 | } 197 | s := &gormSession{tableName: st.opts.TableName} 198 | if err := st.db.Where("id = ? AND expires_at > ?", sessionID, gorm.NowFunc()).First(s).Error; err != nil { 199 | return nil 200 | } 201 | return s 202 | } 203 | return nil 204 | } 205 | 206 | // MaxAge sets the maximum age for the store and the underlying cookie 207 | // implementation. Individual sessions can be deleted by setting 208 | // Options.MaxAge = -1 for that session. 209 | func (st *Store) MaxAge(age int) { 210 | st.SessionOpts.MaxAge = age 211 | for _, codec := range st.Codecs { 212 | if sc, ok := codec.(*securecookie.SecureCookie); ok { 213 | sc.MaxAge(age) 214 | } 215 | } 216 | } 217 | 218 | // MaxLength restricts the maximum length of new sessions to l. 219 | // If l is 0 there is no limit to the size of a session, use with caution. 220 | // The default is 4096 (default for securecookie) 221 | func (st *Store) MaxLength(l int) { 222 | for _, c := range st.Codecs { 223 | if codec, ok := c.(*securecookie.SecureCookie); ok { 224 | codec.MaxLength(l) 225 | } 226 | } 227 | } 228 | 229 | // Cleanup deletes expired sessions 230 | func (st *Store) Cleanup() { 231 | st.db.Delete(&gormSession{tableName: st.opts.TableName}, "expires_at <= ?", gorm.NowFunc()) 232 | } 233 | 234 | // PeriodicCleanup runs Cleanup every interval. Close quit channel to stop. 235 | func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) { 236 | t := time.NewTicker(interval) 237 | defer t.Stop() 238 | for { 239 | select { 240 | case <-t.C: 241 | st.Cleanup() 242 | case <-quit: 243 | return 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /gormstore_test.go: -------------------------------------------------------------------------------- 1 | // TODO: more expire/cleanup tests? 2 | 3 | package gormstore 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "os" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | _ "github.com/go-sql-driver/mysql" 16 | "github.com/jinzhu/gorm" 17 | _ "github.com/lib/pq" 18 | _ "github.com/mattn/go-sqlite3" 19 | ) 20 | 21 | // default test db 22 | var dbURI = "sqlite3://file:dummy?mode=memory&cache=shared" 23 | 24 | // TODO: this is ugly 25 | func parseCookies(value string) map[string]*http.Cookie { 26 | m := map[string]*http.Cookie{} 27 | for _, c := range (&http.Request{Header: http.Header{"Cookie": {value}}}).Cookies() { 28 | m[c.Name] = c 29 | } 30 | return m 31 | } 32 | 33 | func connectDbURI(uri string) (*gorm.DB, error) { 34 | parts := strings.SplitN(uri, "://", 2) 35 | driver := parts[0] 36 | dsn := parts[1] 37 | 38 | var err error 39 | // retry to give some time for db to be ready 40 | for i := 0; i < 50; i++ { 41 | var db *gorm.DB 42 | db, err = gorm.Open(driver, dsn) 43 | if err == nil { 44 | return db, nil 45 | } 46 | time.Sleep(500 * time.Millisecond) 47 | } 48 | 49 | return nil, err 50 | } 51 | 52 | // create new shared in memory db 53 | func newDB() *gorm.DB { 54 | var err error 55 | var db *gorm.DB 56 | if db, err = connectDbURI(dbURI); err != nil { 57 | panic(err) 58 | } 59 | 60 | // db.LogMode(true) 61 | 62 | // cleanup db 63 | if err := db.DropTableIfExists( 64 | &gormSession{tableName: "abc"}, 65 | &gormSession{tableName: "sessions"}, 66 | ).Error; err != nil { 67 | panic(err) 68 | } 69 | 70 | return db 71 | } 72 | 73 | func req(handler http.HandlerFunc, sessionCookie *http.Cookie) *httptest.ResponseRecorder { 74 | req, _ := http.NewRequest("GET", "http://test", nil) 75 | if sessionCookie != nil { 76 | req.Header.Add("Cookie", fmt.Sprintf("%s=%s", sessionCookie.Name, sessionCookie.Value)) 77 | } 78 | w := httptest.NewRecorder() 79 | handler(w, req) 80 | return w 81 | } 82 | 83 | func match(t *testing.T, resp *httptest.ResponseRecorder, code int, body string) { 84 | if resp.Code != code { 85 | t.Errorf("Expected %v, actual %v", code, resp.Code) 86 | } 87 | // http.Error in countHandler adds a \n 88 | if strings.Trim(resp.Body.String(), "\n") != body { 89 | t.Errorf("Expected %v, actual %v", body, resp.Body) 90 | } 91 | } 92 | 93 | func findSession(db *gorm.DB, store *Store, id string) *gormSession { 94 | s := &gormSession{tableName: store.opts.TableName} 95 | if db.Where("id = ?", id).First(s).RecordNotFound() { 96 | return nil 97 | } 98 | return s 99 | } 100 | 101 | func makeCountHandler(name string, store *Store) http.HandlerFunc { 102 | return func(w http.ResponseWriter, r *http.Request) { 103 | session, err := store.Get(r, name) 104 | if err != nil { 105 | panic(err) 106 | } 107 | 108 | count, _ := session.Values["count"].(int) 109 | count++ 110 | session.Values["count"] = count 111 | if err := store.Save(r, w, session); err != nil { 112 | panic(err) 113 | } 114 | // leak session ID so we can mess with it in the db 115 | w.Header().Add("X-Session", session.ID) 116 | http.Error(w, fmt.Sprintf("%d", count), http.StatusOK) 117 | } 118 | } 119 | 120 | func TestBasic(t *testing.T) { 121 | countFn := makeCountHandler("session", New(newDB(), []byte("secret"))) 122 | r1 := req(countFn, nil) 123 | match(t, r1, 200, "1") 124 | r2 := req(countFn, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 125 | match(t, r2, 200, "2") 126 | } 127 | 128 | func TestExpire(t *testing.T) { 129 | db := newDB() 130 | store := New(db, []byte("secret")) 131 | countFn := makeCountHandler("session", store) 132 | 133 | r1 := req(countFn, nil) 134 | match(t, r1, 200, "1") 135 | 136 | // test still in db but expired 137 | id := r1.Header().Get("X-Session") 138 | s := findSession(db, store, id) 139 | s.ExpiresAt = gorm.NowFunc().Add(-40 * 24 * time.Hour) 140 | db.Save(s) 141 | 142 | r2 := req(countFn, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 143 | match(t, r2, 200, "1") 144 | 145 | store.Cleanup() 146 | 147 | if findSession(db, store, id) != nil { 148 | t.Error("Expected session to be deleted") 149 | } 150 | } 151 | 152 | func TestBrokenCookie(t *testing.T) { 153 | db := newDB() 154 | store := New(db, []byte("secret")) 155 | countFn := makeCountHandler("session", store) 156 | 157 | r1 := req(countFn, nil) 158 | match(t, r1, 200, "1") 159 | 160 | cookie := parseCookies(r1.Header().Get("Set-Cookie"))["session"] 161 | cookie.Value += "junk" 162 | r2 := req(countFn, cookie) 163 | match(t, r2, 200, "1") 164 | } 165 | 166 | func TestMaxAgeNegative(t *testing.T) { 167 | db := newDB() 168 | store := New(db, []byte("secret")) 169 | countFn := makeCountHandler("session", store) 170 | 171 | r1 := req(countFn, nil) 172 | match(t, r1, 200, "1") 173 | 174 | r2 := req(func(w http.ResponseWriter, r *http.Request) { 175 | session, err := store.Get(r, "session") 176 | if err != nil { 177 | panic(err) 178 | } 179 | 180 | session.Options.MaxAge = -1 181 | store.Save(r, w, session) 182 | 183 | http.Error(w, "", http.StatusOK) 184 | }, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 185 | 186 | match(t, r2, 200, "") 187 | c := parseCookies(r2.Header().Get("Set-Cookie"))["session"] 188 | if c.Value != "" { 189 | t.Error("Expected empty Set-Cookie session header", c) 190 | } 191 | 192 | id := r1.Header().Get("X-Session") 193 | if s := findSession(db, store, id); s != nil { 194 | t.Error("Expected session to be deleted") 195 | } 196 | } 197 | 198 | func TestMaxLength(t *testing.T) { 199 | store := New(newDB(), []byte("secret")) 200 | store.MaxLength(10) 201 | 202 | r1 := req(func(w http.ResponseWriter, r *http.Request) { 203 | session, err := store.Get(r, "session") 204 | if err != nil { 205 | panic(err) 206 | } 207 | 208 | session.Values["a"] = "aaaaaaaaaaaaaaaaaaaaaaaa" 209 | if err := store.Save(r, w, session); err == nil { 210 | t.Error("Expected too large error") 211 | } 212 | 213 | http.Error(w, "", http.StatusOK) 214 | }, nil) 215 | match(t, r1, 200, "") 216 | } 217 | 218 | func TestTableName(t *testing.T) { 219 | db := newDB() 220 | store := NewOptions(db, Options{TableName: "abc"}, []byte("secret")) 221 | countFn := makeCountHandler("session", store) 222 | 223 | if !db.HasTable(&gormSession{tableName: store.opts.TableName}) { 224 | t.Error("Expected abc table created") 225 | } 226 | 227 | r1 := req(countFn, nil) 228 | match(t, r1, 200, "1") 229 | r2 := req(countFn, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 230 | match(t, r2, 200, "2") 231 | 232 | id := r2.Header().Get("X-Session") 233 | s := findSession(db, store, id) 234 | s.ExpiresAt = gorm.NowFunc().Add(-time.Duration(store.SessionOpts.MaxAge+1) * time.Second) 235 | db.Save(s) 236 | 237 | store.Cleanup() 238 | 239 | if findSession(db, store, id) != nil { 240 | t.Error("Expected session to be deleted") 241 | } 242 | } 243 | 244 | func TestSkipCreateTable(t *testing.T) { 245 | db := newDB() 246 | store := NewOptions(db, Options{SkipCreateTable: true}, []byte("secret")) 247 | 248 | if db.HasTable(&gormSession{tableName: store.opts.TableName}) { 249 | t.Error("Expected no table created") 250 | } 251 | } 252 | 253 | func TestMultiSessions(t *testing.T) { 254 | store := New(newDB(), []byte("secret")) 255 | countFn1 := makeCountHandler("session1", store) 256 | countFn2 := makeCountHandler("session2", store) 257 | 258 | r1 := req(countFn1, nil) 259 | match(t, r1, 200, "1") 260 | r2 := req(countFn2, nil) 261 | match(t, r2, 200, "1") 262 | 263 | r3 := req(countFn1, parseCookies(r1.Header().Get("Set-Cookie"))["session1"]) 264 | match(t, r3, 200, "2") 265 | r4 := req(countFn2, parseCookies(r2.Header().Get("Set-Cookie"))["session2"]) 266 | match(t, r4, 200, "2") 267 | } 268 | 269 | func TestPeriodicCleanup(t *testing.T) { 270 | db := newDB() 271 | store := New(db, []byte("secret")) 272 | store.SessionOpts.MaxAge = 1 273 | countFn := makeCountHandler("session", store) 274 | 275 | quit := make(chan struct{}) 276 | go store.PeriodicCleanup(200*time.Millisecond, quit) 277 | 278 | // test that cleanup i done at least twice 279 | 280 | r1 := req(countFn, nil) 281 | id1 := r1.Header().Get("X-Session") 282 | 283 | if findSession(db, store, id1) == nil { 284 | t.Error("Expected r1 session to exist") 285 | } 286 | 287 | time.Sleep(2 * time.Second) 288 | 289 | if findSession(db, store, id1) != nil { 290 | t.Error("Expected r1 session to be deleted") 291 | } 292 | 293 | r2 := req(countFn, nil) 294 | id2 := r2.Header().Get("X-Session") 295 | 296 | if findSession(db, store, id2) == nil { 297 | t.Error("Expected r2 session to exist") 298 | } 299 | 300 | time.Sleep(2 * time.Second) 301 | 302 | if findSession(db, store, id2) != nil { 303 | t.Error("Expected r2 session to be deleted") 304 | } 305 | 306 | close(quit) 307 | 308 | // test that cleanup has stopped 309 | 310 | r3 := req(countFn, nil) 311 | id3 := r3.Header().Get("X-Session") 312 | 313 | if findSession(db, store, id3) == nil { 314 | t.Error("Expected r3 session to exist") 315 | } 316 | 317 | time.Sleep(2 * time.Second) 318 | 319 | if findSession(db, store, id3) == nil { 320 | t.Error("Expected r3 session to exist") 321 | } 322 | } 323 | 324 | func TestMain(m *testing.M) { 325 | flag.Parse() 326 | 327 | if v := os.Getenv("DATABASE_URI"); v != "" { 328 | dbURI = v 329 | } 330 | fmt.Printf("DATABASE_URI=%s\n", dbURI) 331 | 332 | os.Exit(m.Run()) 333 | } 334 | -------------------------------------------------------------------------------- /test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_IP=${DOCKER_IP:-127.0.0.1} 4 | 5 | sqlite3() { 6 | DATABASE_URI="sqlite3://file:dummy?mode=memory&cache=shared" go test -v -race -cover 7 | return $? 8 | } 9 | 10 | postgres() { 11 | VERSION=$1 12 | ID=$(docker run \ 13 | -e POSTGRES_USER=postgres \ 14 | -e POSTGRES_PASSWORD=postgres \ 15 | -p 5432 -d postgres:$VERSION) 16 | PORT=$(docker port "$ID" 5432 | cut -d : -f 2) 17 | DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover 18 | S=$? 19 | docker rm -vf "$ID" > /dev/null 20 | return $S 21 | } 22 | 23 | mysql() { 24 | VERSION=$1 25 | ID=$(docker run \ 26 | -e MYSQL_ROOT_PASSWORD=root \ 27 | -e MYSQL_USER=mysql \ 28 | -e MYSQL_PASSWORD=mysql \ 29 | -e MYSQL_DATABASE=mysql \ 30 | -p 3306 -d mysql:$VERSION) 31 | PORT=$(docker port "$ID" 3306 | cut -d : -f 2) 32 | DATABASE_URI="mysql://mysql:mysql@tcp($DOCKER_IP:$PORT)/mysql?charset=utf8&parseTime=True" go test -v -race -cover 33 | S=$? 34 | docker rm -vf "$ID" > /dev/null 35 | return $S 36 | } 37 | 38 | sqlite3 || exit 1 39 | postgres 9.4 || exit 1 40 | postgres 9.6 || exit 1 41 | postgres 10 || exit 1 42 | postgres 11 || exit 1 43 | postgres 12 || exit 1 44 | postgres 13 || exit 1 45 | postgres 14 || exit 1 46 | postgres 15 || exit 1 47 | mysql 5.7 || exit 1 48 | mysql 8.0 || exit 1 49 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wader/gormstore/v2 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/gorilla/securecookie v1.1.2 7 | // bump: gorilla/sessions /github\.com\/gorilla\/sessions v(.*)/ https://github.com/gorilla/sessions.git|^1 8 | // bump: gorilla/sessions command cd v2 && go get -d github.com/gorilla/sessions@v$LATEST && go mod tidy 9 | github.com/gorilla/sessions v1.3.0 10 | github.com/mattn/go-sqlite3 v1.14.16 // indirect 11 | gorm.io/driver/mysql v1.4.7 12 | gorm.io/driver/postgres v1.4.8 13 | gorm.io/driver/sqlite v1.4.4 14 | // bump: gorm.io/gorm /gorm\.io\/gorm v(.*)/ https://github.com/go-gorm/gorm.git|^1 15 | // bump: gorm.io/gorm command cd v2 && go get -d gorm.io/gorm@v$LATEST && go mod tidy 16 | gorm.io/gorm v1.26.1 17 | ) 18 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= 6 | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 7 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 9 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 10 | github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= 11 | github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 12 | github.com/gorilla/sessions v1.3.0 h1:XYlkq7KcpOB2ZhHBPv5WpjMIxrQosiZanfoy1HLZFzg= 13 | github.com/gorilla/sessions v1.3.0/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= 14 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 15 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 16 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 17 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 18 | github.com/jackc/pgx/v5 v5.3.0 h1:/NQi8KHMpKWHInxXesC8yD4DhkXPrVhmnwYkjp9AmBA= 19 | github.com/jackc/pgx/v5 v5.3.0/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= 20 | github.com/jackc/puddle/v2 v2.2.0/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 21 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 22 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 23 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 24 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 25 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 26 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 27 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 28 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 29 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 30 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 31 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 32 | github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 33 | github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= 34 | github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= 35 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 36 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 40 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 41 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 42 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 43 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 44 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 45 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 46 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 47 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 48 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 49 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 50 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 51 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 52 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 53 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= 54 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 55 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 56 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 57 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 58 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 59 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 62 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 63 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 64 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 65 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 66 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 67 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 68 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 69 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 72 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 73 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 74 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 75 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 78 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 81 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 83 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 84 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 85 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 86 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 87 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 88 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 89 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 90 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 91 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 92 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 93 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 94 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 95 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 96 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 97 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 98 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 99 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 100 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 101 | golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= 102 | golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= 103 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 104 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 105 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 106 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 107 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 108 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 109 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 110 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 111 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 112 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 113 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 114 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 115 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 116 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 117 | gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= 118 | gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc= 119 | gorm.io/driver/postgres v1.4.8 h1:NDWizaclb7Q2aupT0jkwK8jx1HVCNzt+PQ8v/VnxviA= 120 | gorm.io/driver/postgres v1.4.8/go.mod h1:O9MruWGNLUBUWVYfWuBClpf3HeGjOoybY0SNmCs3wsw= 121 | gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= 122 | gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= 123 | gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 124 | gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 125 | gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 126 | gorm.io/gorm v1.26.1 h1:ghB2gUI9FkS46luZtn6DLZ0f6ooBJ5IbVej2ENFDjRw= 127 | gorm.io/gorm v1.26.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 128 | -------------------------------------------------------------------------------- /v2/gormstore.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gormstore is a GORM backend for gorilla sessions 3 | 4 | Simplest form: 5 | 6 | store := gormstore.New(gorm.Open(...), []byte("secret-hash-key")) 7 | 8 | All options: 9 | 10 | store := gormstore.NewOptions( 11 | gorm.Open(...), // *gorm.DB 12 | gormstore.Options{ 13 | TableName: "sessions", // "sessions" is default 14 | SkipCreateTable: false, // false is default 15 | }, 16 | []byte("secret-hash-key"), // 32 or 64 bytes recommended, required 17 | []byte("secret-encryption-key")) // nil, 16, 24 or 32 bytes, optional 18 | 19 | // some more settings, see sessions.Options 20 | store.SessionOpts.Secure = true 21 | store.SessionOpts.HttpOnly = true 22 | store.SessionOpts.MaxAge = 60 * 60 * 24 * 60 23 | 24 | If you want periodic cleanup of expired sessions: 25 | 26 | quit := make(chan struct{}) 27 | go store.PeriodicCleanup(1*time.Hour, quit) 28 | 29 | For more information about the keys see https://github.com/gorilla/securecookie 30 | 31 | For API to use in HTTP handlers see https://github.com/gorilla/sessions 32 | */ 33 | package gormstore 34 | 35 | import ( 36 | "encoding/base32" 37 | "net/http" 38 | "strings" 39 | "time" 40 | 41 | "github.com/gorilla/securecookie" 42 | "github.com/gorilla/sessions" 43 | "gorm.io/gorm" 44 | ) 45 | 46 | const sessionIDLen = 32 47 | const defaultTableName = "sessions" 48 | const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days 49 | const defaultPath = "/" 50 | 51 | // Options for gormstore 52 | type Options struct { 53 | TableName string 54 | SkipCreateTable bool 55 | } 56 | 57 | // Store represent a gormstore 58 | type Store struct { 59 | db *gorm.DB 60 | opts Options 61 | Codecs []securecookie.Codec 62 | SessionOpts *sessions.Options 63 | } 64 | 65 | type gormSession struct { 66 | ID string `sql:"unique_index"` 67 | Data string `sql:"type:text"` 68 | CreatedAt time.Time 69 | UpdatedAt time.Time 70 | ExpiresAt time.Time `sql:"index"` 71 | } 72 | 73 | // New creates a new gormstore session 74 | func New(db *gorm.DB, keyPairs ...[]byte) *Store { 75 | return NewOptions(db, Options{}, keyPairs...) 76 | } 77 | 78 | // NewOptions creates a new gormstore session with options 79 | func NewOptions(db *gorm.DB, opts Options, keyPairs ...[]byte) *Store { 80 | st := &Store{ 81 | db: db, 82 | opts: opts, 83 | Codecs: securecookie.CodecsFromPairs(keyPairs...), 84 | SessionOpts: &sessions.Options{ 85 | Path: defaultPath, 86 | MaxAge: defaultMaxAge, 87 | }, 88 | } 89 | if st.opts.TableName == "" { 90 | st.opts.TableName = defaultTableName 91 | } 92 | 93 | if !st.opts.SkipCreateTable { 94 | st.sessionTable().AutoMigrate(&gormSession{}) 95 | } 96 | 97 | return st 98 | } 99 | 100 | func (st *Store) sessionTable() *gorm.DB { 101 | return st.db.Table(st.opts.TableName) 102 | } 103 | 104 | // Get returns a session for the given name after adding it to the registry. 105 | func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) { 106 | return sessions.GetRegistry(r).Get(st, name) 107 | } 108 | 109 | // New creates a session with name without adding it to the registry. 110 | func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) { 111 | session := sessions.NewSession(st, name) 112 | opts := *st.SessionOpts 113 | session.Options = &opts 114 | session.IsNew = true 115 | 116 | st.MaxAge(st.SessionOpts.MaxAge) 117 | 118 | // try fetch from db if there is a cookie 119 | s := st.getSessionFromCookie(r, session.Name()) 120 | if s != nil { 121 | if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil { 122 | return session, nil 123 | } 124 | session.ID = s.ID 125 | session.IsNew = false 126 | } 127 | 128 | return session, nil 129 | } 130 | 131 | // Save session and set cookie header 132 | func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { 133 | s := st.getSessionFromCookie(r, session.Name()) 134 | 135 | // delete if max age is < 0 136 | if session.Options.MaxAge < 0 { 137 | if s != nil { 138 | if err := st.sessionTable().Delete(s).Error; err != nil { 139 | return err 140 | } 141 | } 142 | http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) 143 | return nil 144 | } 145 | 146 | data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...) 147 | if err != nil { 148 | return err 149 | } 150 | now := time.Now() 151 | expire := now.Add(time.Second * time.Duration(session.Options.MaxAge)) 152 | 153 | if s == nil { 154 | // generate random session ID key suitable for storage in the db 155 | session.ID = strings.TrimRight( 156 | base32.StdEncoding.EncodeToString( 157 | securecookie.GenerateRandomKey(sessionIDLen)), "=") 158 | s = &gormSession{ 159 | ID: session.ID, 160 | Data: data, 161 | CreatedAt: now, 162 | UpdatedAt: now, 163 | ExpiresAt: expire, 164 | } 165 | if err := st.sessionTable().Create(s).Error; err != nil { 166 | return err 167 | } 168 | } else { 169 | s.Data = data 170 | s.UpdatedAt = now 171 | s.ExpiresAt = expire 172 | if err := st.sessionTable().Save(s).Error; err != nil { 173 | return err 174 | } 175 | } 176 | 177 | // set session id cookie 178 | id, err := securecookie.EncodeMulti(session.Name(), s.ID, st.Codecs...) 179 | if err != nil { 180 | return err 181 | } 182 | http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options)) 183 | 184 | return nil 185 | } 186 | 187 | // getSessionFromCookie looks for an existing gormSession from a session ID stored inside a cookie 188 | func (st *Store) getSessionFromCookie(r *http.Request, name string) *gormSession { 189 | if cookie, err := r.Cookie(name); err == nil { 190 | sessionID := "" 191 | if err := securecookie.DecodeMulti(name, cookie.Value, &sessionID, st.Codecs...); err != nil { 192 | return nil 193 | } 194 | s := &gormSession{} 195 | sr := st.sessionTable().Where("id = ? AND expires_at > ?", sessionID, time.Now()).Limit(1).Find(s) 196 | if sr.Error != nil || sr.RowsAffected == 0 { 197 | return nil 198 | } 199 | return s 200 | } 201 | return nil 202 | } 203 | 204 | // MaxAge sets the maximum age for the store and the underlying cookie 205 | // implementation. Individual sessions can be deleted by setting 206 | // Options.MaxAge = -1 for that session. 207 | func (st *Store) MaxAge(age int) { 208 | st.SessionOpts.MaxAge = age 209 | for _, codec := range st.Codecs { 210 | if sc, ok := codec.(*securecookie.SecureCookie); ok { 211 | sc.MaxAge(age) 212 | } 213 | } 214 | } 215 | 216 | // MaxLength restricts the maximum length of new sessions to l. 217 | // If l is 0 there is no limit to the size of a session, use with caution. 218 | // The default is 4096 (default for securecookie) 219 | func (st *Store) MaxLength(l int) { 220 | for _, c := range st.Codecs { 221 | if codec, ok := c.(*securecookie.SecureCookie); ok { 222 | codec.MaxLength(l) 223 | } 224 | } 225 | } 226 | 227 | // Cleanup deletes expired sessions 228 | func (st *Store) Cleanup() { 229 | st.sessionTable().Delete(&gormSession{}, "expires_at <= ?", time.Now()) 230 | } 231 | 232 | // PeriodicCleanup runs Cleanup every interval. Close quit channel to stop. 233 | func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) { 234 | t := time.NewTicker(interval) 235 | defer t.Stop() 236 | for { 237 | select { 238 | case <-t.C: 239 | st.Cleanup() 240 | case <-quit: 241 | return 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /v2/gormstore_test.go: -------------------------------------------------------------------------------- 1 | package gormstore 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "gorm.io/driver/mysql" 14 | "gorm.io/driver/postgres" 15 | "gorm.io/driver/sqlite" 16 | "gorm.io/gorm" 17 | ) 18 | 19 | // default test db 20 | var dbURI = "sqlite3://file:dummy?mode=memory&cache=shared" 21 | 22 | // TODO: this is ugly 23 | func parseCookies(value string) map[string]*http.Cookie { 24 | m := map[string]*http.Cookie{} 25 | for _, c := range (&http.Request{Header: http.Header{"Cookie": {value}}}).Cookies() { 26 | m[c.Name] = c 27 | } 28 | return m 29 | } 30 | 31 | func uriToDialector(uri string) (gorm.Dialector, error) { 32 | parts := strings.SplitN(uri, "://", 2) 33 | driver := parts[0] 34 | dsn := parts[1] 35 | 36 | switch driver { 37 | case "sqlite3": 38 | return sqlite.Open(dsn), nil 39 | case "postgres": 40 | return postgres.Open(dsn), nil 41 | case "mysql": 42 | return mysql.Open(dsn), nil 43 | } 44 | 45 | return nil, fmt.Errorf("unknown driver %s", driver) 46 | } 47 | 48 | func connectDbURI(uri string) (*gorm.DB, error) { 49 | dialect, err := uriToDialector(uri) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | // retry to give some time for db to be ready 55 | for i := 0; i < 50; i++ { 56 | var db *gorm.DB 57 | db, err = gorm.Open(dialect, &gorm.Config{}) 58 | if err == nil { 59 | return db, nil 60 | } 61 | time.Sleep(500 * time.Millisecond) 62 | } 63 | 64 | return nil, err 65 | } 66 | 67 | // create new shared in memory db 68 | func newDB() *gorm.DB { 69 | var err error 70 | var db *gorm.DB 71 | if db, err = connectDbURI(dbURI); err != nil { 72 | panic(err) 73 | } 74 | 75 | //db = db.Debug() 76 | 77 | // cleanup db 78 | // TODO: check error if non not-exist err? 79 | db.Migrator().DropTable("abc") 80 | db.Migrator().DropTable("sessions") 81 | 82 | return db 83 | } 84 | 85 | func req(handler http.HandlerFunc, sessionCookie *http.Cookie) *httptest.ResponseRecorder { 86 | req, _ := http.NewRequest("GET", "http://test", nil) 87 | if sessionCookie != nil { 88 | req.Header.Add("Cookie", fmt.Sprintf("%s=%s", sessionCookie.Name, sessionCookie.Value)) 89 | } 90 | w := httptest.NewRecorder() 91 | handler(w, req) 92 | return w 93 | } 94 | 95 | func match(t *testing.T, resp *httptest.ResponseRecorder, code int, body string) { 96 | if resp.Code != code { 97 | t.Errorf("Expected %v, actual %v", code, resp.Code) 98 | } 99 | // http.Error in countHandler adds a \n 100 | if strings.Trim(resp.Body.String(), "\n") != body { 101 | t.Errorf("Expected %v, actual %v", body, resp.Body) 102 | } 103 | } 104 | 105 | func findSession(db *gorm.DB, store *Store, id string) *gormSession { 106 | s := &gormSession{} 107 | sr := store.sessionTable().Where("id = ?", id).Limit(1).Find(s) 108 | if sr.Error != nil || sr.RowsAffected == 0 { 109 | return nil 110 | } 111 | return s 112 | } 113 | 114 | func makeCountHandler(name string, store *Store) http.HandlerFunc { 115 | return func(w http.ResponseWriter, r *http.Request) { 116 | session, err := store.Get(r, name) 117 | if err != nil { 118 | panic(err) 119 | } 120 | 121 | count, _ := session.Values["count"].(int) 122 | count++ 123 | session.Values["count"] = count 124 | if err := store.Save(r, w, session); err != nil { 125 | panic(err) 126 | } 127 | // leak session ID so we can mess with it in the db 128 | w.Header().Add("X-Session", session.ID) 129 | http.Error(w, fmt.Sprintf("%d", count), http.StatusOK) 130 | } 131 | } 132 | 133 | func TestBasic(t *testing.T) { 134 | countFn := makeCountHandler("session", New(newDB(), []byte("secret"))) 135 | r1 := req(countFn, nil) 136 | match(t, r1, 200, "1") 137 | r2 := req(countFn, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 138 | match(t, r2, 200, "2") 139 | } 140 | 141 | func TestExpire(t *testing.T) { 142 | db := newDB() 143 | store := New(db, []byte("secret")) 144 | countFn := makeCountHandler("session", store) 145 | 146 | r1 := req(countFn, nil) 147 | match(t, r1, 200, "1") 148 | 149 | // test still in db but expired 150 | id := r1.Header().Get("X-Session") 151 | s := findSession(db, store, id) 152 | 153 | s.ExpiresAt = time.Now().Add(-40 * 24 * time.Hour) 154 | store.sessionTable().Save(s) 155 | 156 | r2 := req(countFn, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 157 | match(t, r2, 200, "1") 158 | 159 | store.Cleanup() 160 | 161 | if findSession(db, store, id) != nil { 162 | t.Error("Expected session to be deleted") 163 | } 164 | } 165 | 166 | func TestBrokenCookie(t *testing.T) { 167 | db := newDB() 168 | store := New(db, []byte("secret")) 169 | countFn := makeCountHandler("session", store) 170 | 171 | r1 := req(countFn, nil) 172 | match(t, r1, 200, "1") 173 | 174 | cookie := parseCookies(r1.Header().Get("Set-Cookie"))["session"] 175 | cookie.Value += "junk" 176 | r2 := req(countFn, cookie) 177 | match(t, r2, 200, "1") 178 | } 179 | 180 | func TestMaxAgeNegative(t *testing.T) { 181 | db := newDB() 182 | store := New(db, []byte("secret")) 183 | countFn := makeCountHandler("session", store) 184 | 185 | r1 := req(countFn, nil) 186 | match(t, r1, 200, "1") 187 | 188 | r2 := req(func(w http.ResponseWriter, r *http.Request) { 189 | session, err := store.Get(r, "session") 190 | if err != nil { 191 | panic(err) 192 | } 193 | 194 | session.Options.MaxAge = -1 195 | store.Save(r, w, session) 196 | 197 | http.Error(w, "", http.StatusOK) 198 | }, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 199 | 200 | match(t, r2, 200, "") 201 | c := parseCookies(r2.Header().Get("Set-Cookie"))["session"] 202 | if c.Value != "" { 203 | t.Error("Expected empty Set-Cookie session header", c) 204 | } 205 | 206 | id := r1.Header().Get("X-Session") 207 | if s := findSession(db, store, id); s != nil { 208 | t.Error("Expected session to be deleted") 209 | } 210 | } 211 | 212 | func TestMaxLength(t *testing.T) { 213 | store := New(newDB(), []byte("secret")) 214 | store.MaxLength(10) 215 | 216 | r1 := req(func(w http.ResponseWriter, r *http.Request) { 217 | session, err := store.Get(r, "session") 218 | if err != nil { 219 | panic(err) 220 | } 221 | 222 | session.Values["a"] = "aaaaaaaaaaaaaaaaaaaaaaaa" 223 | if err := store.Save(r, w, session); err == nil { 224 | t.Error("Expected too large error") 225 | } 226 | 227 | http.Error(w, "", http.StatusOK) 228 | }, nil) 229 | match(t, r1, 200, "") 230 | } 231 | 232 | func TestTableName(t *testing.T) { 233 | db := newDB() 234 | store := NewOptions(db, Options{TableName: "abc"}, []byte("secret")) 235 | countFn := makeCountHandler("session", store) 236 | 237 | if !db.Migrator().HasTable(store.opts.TableName) { 238 | t.Error("Expected abc table created") 239 | } 240 | 241 | r1 := req(countFn, nil) 242 | match(t, r1, 200, "1") 243 | r2 := req(countFn, parseCookies(r1.Header().Get("Set-Cookie"))["session"]) 244 | match(t, r2, 200, "2") 245 | 246 | id := r2.Header().Get("X-Session") 247 | s := findSession(db, store, id) 248 | s.ExpiresAt = time.Now().Add(-time.Duration(store.SessionOpts.MaxAge+1) * time.Second) 249 | store.sessionTable().Save(s) 250 | 251 | store.Cleanup() 252 | 253 | if findSession(db, store, id) != nil { 254 | t.Error("Expected session to be deleted") 255 | } 256 | } 257 | 258 | func TestSkipCreateTable(t *testing.T) { 259 | db := newDB() 260 | store := NewOptions(db, Options{SkipCreateTable: true}, []byte("secret")) 261 | 262 | if db.Migrator().HasTable(store.opts.TableName) { 263 | t.Error("Expected no table created") 264 | } 265 | } 266 | 267 | func TestMultiSessions(t *testing.T) { 268 | store := New(newDB(), []byte("secret")) 269 | countFn1 := makeCountHandler("session1", store) 270 | countFn2 := makeCountHandler("session2", store) 271 | 272 | r1 := req(countFn1, nil) 273 | match(t, r1, 200, "1") 274 | r2 := req(countFn2, nil) 275 | match(t, r2, 200, "1") 276 | 277 | r3 := req(countFn1, parseCookies(r1.Header().Get("Set-Cookie"))["session1"]) 278 | match(t, r3, 200, "2") 279 | r4 := req(countFn2, parseCookies(r2.Header().Get("Set-Cookie"))["session2"]) 280 | match(t, r4, 200, "2") 281 | } 282 | 283 | func TestReuseSessionByName(t *testing.T) { 284 | db := newDB() 285 | store := New(db, []byte("secret")) 286 | sessionName := "test-session" 287 | 288 | handler := func(w http.ResponseWriter, r *http.Request) { 289 | session, err := store.New(r, sessionName) 290 | if err != nil { 291 | panic(err) 292 | } 293 | session.ID = "" 294 | if err := store.Save(r, w, session); err != nil { 295 | panic(err) 296 | } 297 | http.Error(w, "", http.StatusOK) 298 | } 299 | 300 | r1 := req(handler, nil) 301 | match(t, r1, 200, "") 302 | r2 := req(handler, parseCookies(r1.Header().Get("Set-Cookie"))[sessionName]) 303 | match(t, r2, 200, "") 304 | 305 | var count int64 306 | store.sessionTable().Count(&count) 307 | if count > 1 { 308 | t.Error("An existing session with the same name should be reused") 309 | } 310 | } 311 | 312 | func TestPeriodicCleanup(t *testing.T) { 313 | db := newDB() 314 | store := New(db, []byte("secret")) 315 | store.SessionOpts.MaxAge = 1 316 | countFn := makeCountHandler("session", store) 317 | 318 | quit := make(chan struct{}) 319 | go store.PeriodicCleanup(200*time.Millisecond, quit) 320 | 321 | // test that cleanup i done at least twice 322 | 323 | r1 := req(countFn, nil) 324 | id1 := r1.Header().Get("X-Session") 325 | 326 | if findSession(db, store, id1) == nil { 327 | t.Error("Expected r1 session to exist") 328 | } 329 | 330 | time.Sleep(2 * time.Second) 331 | 332 | if findSession(db, store, id1) != nil { 333 | t.Error("Expected r1 session to be deleted") 334 | } 335 | 336 | r2 := req(countFn, nil) 337 | id2 := r2.Header().Get("X-Session") 338 | 339 | if findSession(db, store, id2) == nil { 340 | t.Error("Expected r2 session to exist") 341 | } 342 | 343 | time.Sleep(2 * time.Second) 344 | 345 | if findSession(db, store, id2) != nil { 346 | t.Error("Expected r2 session to be deleted") 347 | } 348 | 349 | close(quit) 350 | 351 | // test that cleanup has stopped 352 | 353 | r3 := req(countFn, nil) 354 | id3 := r3.Header().Get("X-Session") 355 | 356 | if findSession(db, store, id3) == nil { 357 | t.Error("Expected r3 session to exist") 358 | } 359 | 360 | time.Sleep(2 * time.Second) 361 | 362 | if findSession(db, store, id3) == nil { 363 | t.Error("Expected r3 session to exist") 364 | } 365 | } 366 | 367 | func TestMain(m *testing.M) { 368 | flag.Parse() 369 | 370 | if v := os.Getenv("DATABASE_URI"); v != "" { 371 | dbURI = v 372 | } 373 | fmt.Printf("DATABASE_URI=%s\n", dbURI) 374 | 375 | os.Exit(m.Run()) 376 | } 377 | -------------------------------------------------------------------------------- /v2/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DOCKER_IP=${DOCKER_IP:-127.0.0.1} 4 | 5 | sqlite3() { 6 | DATABASE_URI="sqlite3://file:dummy?mode=memory&cache=shared" go test -v -race -cover 7 | return $? 8 | } 9 | 10 | postgres() { 11 | VERSION=$1 12 | ID=$(docker run \ 13 | -e POSTGRES_USER=postgres \ 14 | -e POSTGRES_PASSWORD=postgres \ 15 | -p 5432 -d postgres:$VERSION) 16 | PORT=$(docker port "$ID" 5432 | cut -d : -f 2) 17 | DATABASE_URI="postgres://user=postgres password=postgres dbname=postgres host=$DOCKER_IP port=$PORT sslmode=disable" go test -v -race -cover 18 | S=$? 19 | docker rm -vf "$ID" > /dev/null 20 | return $S 21 | } 22 | 23 | mysql() { 24 | VERSION=$1 25 | ID=$(docker run \ 26 | -e MYSQL_ROOT_PASSWORD=root \ 27 | -e MYSQL_USER=mysql \ 28 | -e MYSQL_PASSWORD=mysql \ 29 | -e MYSQL_DATABASE=mysql \ 30 | -p 3306 -d mysql:$VERSION) 31 | PORT=$(docker port "$ID" 3306 | cut -d : -f 2) 32 | DATABASE_URI="mysql://mysql:mysql@tcp($DOCKER_IP:$PORT)/mysql?charset=utf8&parseTime=True" go test -v -race -cover 33 | S=$? 34 | docker rm -vf "$ID" > /dev/null 35 | return $S 36 | } 37 | 38 | sqlite3 || exit 1 39 | postgres 9.4 || exit 1 40 | postgres 9.6 || exit 1 41 | postgres 10 || exit 1 42 | postgres 11 || exit 1 43 | postgres 12 || exit 1 44 | postgres 13 || exit 1 45 | postgres 14 || exit 1 46 | postgres 15 || exit 1 47 | mysql 5.7 || exit 1 48 | mysql 8.0 || exit 1 49 | --------------------------------------------------------------------------------