├── .DS_Store ├── .gitattributes ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── scripts │ └── setup_examples_test.bash └── workflows │ ├── ci.yml │ └── codeql-analysis.yml ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── _examples ├── database │ ├── redis │ │ └── main.go │ └── rediscluster │ │ └── main.go ├── fasthttp │ ├── README.md │ └── main.go ├── flash-messages │ └── main.go ├── overview │ └── main.go ├── securecookie │ └── main.go └── standalone │ └── main.go ├── config.go ├── cookie.go ├── database.go ├── go.mod ├── go.sum ├── lifetime.go ├── logo_900_273_bg_white.png ├── memstore.go ├── provider.go ├── session.go ├── sessiondb ├── badger │ └── database.go ├── boltdb │ └── database.go ├── redis │ ├── database.go │ └── service │ │ ├── config.go │ │ └── service.go └── rediscluster │ ├── database.go │ └── service │ ├── config.go │ └── service.go ├── sessions.go ├── sessions_test.go └── transcoding.go /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/go-sessions/2a50e79ae6a46b0cca3659682de8a582aa97dd71/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go linguist-language=Go 2 | vendor/* linguist-vendored 3 | _examples/* linguist-documentation 4 | _benchmarks/* linguist-documentation 5 | # Set the default behavior, in case people don't have core.autocrlf set. 6 | # if from windows: 7 | # git config --global core.autocrlf true 8 | # if from unix: 9 | # git config --global core.autocrlf input 10 | # https://help.github.com/articles/dealing-with-line-endings/#per-repository-settings 11 | * text=auto -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | * @kataras 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: kataras -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # We'd love to see more contributions 2 | 3 | Read how you can [contribute to the project](https://github.com/kataras/go-sessions/blob/master/CONTRIBUTING.md). 4 | 5 | > Please attach an [issue](https://github.com/kataras/go-sessions/issues) link which your PR solves otherwise your work may be rejected. 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/scripts/setup_examples_test.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for f in ../../_examples/*; do 4 | if [ -d "$f" ]; then 5 | # Will not run if no directories are available 6 | go mod init 7 | go get -u github.com/kataras/go-sessions/v3@master 8 | go mod download 9 | go run . 10 | fi 11 | done 12 | 13 | # git update-index --chmod=+x ./.github/scripts/setup_examples_test.bash -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | 14 | test: 15 | name: Test 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | matrix: 20 | go_version: [1.19.x] 21 | steps: 22 | 23 | - name: Set up Go 1.x 24 | uses: actions/setup-go@v2 25 | with: 26 | go-version: ${{ matrix.go_version }} 27 | 28 | - name: Check out code into the Go module directory 29 | uses: actions/checkout@v2 30 | 31 | - name: Test 32 | run: go test -v ./... 33 | 34 | - name: Setup examples for testing 35 | run: ./.github/scripts/setup_examples_test.bash 36 | 37 | - name: Test examples 38 | continue-on-error: true 39 | working-directory: _examples 40 | run: go test -v -mod=mod -cover -race ./... 41 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '24 11 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go'] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v2 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | /_examples/**/node_modules 3 | /_examples/issue-* 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the official list of Iris Sessions authors for copyright 2 | # purposes. 3 | 4 | Gerasimos Maropoulos -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Gerasimos Maropoulos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | Build Status 7 | License 8 | Releases 9 | Read me docs 10 | Build Status 11 | Built with GoLang 12 | Platforms 13 |
14 | 15 | Fast http sessions manager for Go.
16 | Simple API, while providing robust set of features such as immutability, expiration time (can be shifted), [databases](sessiondb) like badger and redis as back-end storage.
17 | 18 |

19 | 20 | Quick view 21 | ----------- 22 | 23 | ```go 24 | import "github.com/kataras/go-sessions/v3" 25 | 26 | sess := sessions.Start(http.ResponseWriter, *http.Request) 27 | sess. 28 | ID() string 29 | Get(string) interface{} 30 | HasFlash() bool 31 | GetFlash(string) interface{} 32 | GetFlashString(string) string 33 | GetString(key string) string 34 | GetInt(key string) (int, error) 35 | GetInt64(key string) (int64, error) 36 | GetFloat32(key string) (float32, error) 37 | GetFloat64(key string) (float64, error) 38 | GetBoolean(key string) (bool, error) 39 | GetAll() map[string]interface{} 40 | GetFlashes() map[string]interface{} 41 | VisitAll(cb func(k string, v interface{})) 42 | Set(string, interface{}) 43 | SetImmutable(key string, value interface{}) 44 | SetFlash(string, interface{}) 45 | Delete(string) 46 | Clear() 47 | ClearFlashes() 48 | ``` 49 | 50 | Installation 51 | ------------ 52 | 53 | The only requirement is the [Go Programming Language](https://golang.org/dl), at least 1.14. 54 | 55 | ```bash 56 | $ go get github.com/kataras/go-sessions/v3 57 | ``` 58 | 59 | **go.mod** 60 | 61 | ```mod 62 | module your_app 63 | 64 | go 1.19 65 | 66 | require ( 67 | github.com/kataras/go-sessions/v3 v3.3.1 68 | ) 69 | ``` 70 | 71 | Features 72 | ------------ 73 | 74 | - Focus on simplicity and performance. 75 | - Flash messages. 76 | - Supports any type of [external database](_examples/database). 77 | - Works with both [net/http](https://golang.org/pkg/net/http/) and [valyala/fasthttp](https://github.com/valyala/fasthttp). 78 | 79 | Documentation 80 | ------------ 81 | 82 | Take a look at the [./examples](https://github.com/kataras/go-sessions/tree/master/_examples) folder. 83 | 84 | - [Overview](_examples/overview/main.go) 85 | - [Standalone](_examples/standalone/main.go) 86 | - [Fasthttp](_examples/fasthttp/main.go) 87 | - [Secure Cookie](_examples/securecookie/main.go) 88 | - [Flash Messages](_examples/flash-messages/main.go) 89 | - [Databases](_examples/database) 90 | * [Redis](_examples/database/redis/main.go) 91 | 92 | Outline 93 | ------------ 94 | 95 | ```go 96 | // Start starts the session for the particular net/http request 97 | Start(w http.ResponseWriter,r *http.Request) Session 98 | // ShiftExpiration move the expire date of a session to a new date 99 | // by using session default timeout configuration. 100 | ShiftExpiration(w http.ResponseWriter, r *http.Request) 101 | // UpdateExpiration change expire date of a session to a new date 102 | // by using timeout value passed by `expires` receiver. 103 | UpdateExpiration(w http.ResponseWriter, r *http.Request, expires time.Duration) 104 | // Destroy kills the net/http session and remove the associated cookie 105 | Destroy(w http.ResponseWriter,r *http.Request) 106 | 107 | // Start starts the session for the particular valyala/fasthttp request 108 | StartFasthttp(ctx *fasthttp.RequestCtx) Session 109 | // ShiftExpirationFasthttp move the expire date of a session to a new date 110 | // by using session default timeout configuration. 111 | ShiftExpirationFasthttp(ctx *fasthttp.RequestCtx) 112 | // UpdateExpirationFasthttp change expire date of a session to a new date 113 | // by using timeout value passed by `expires` receiver. 114 | UpdateExpirationFasthttp(ctx *fasthttp.RequestCtx, expires time.Duration) 115 | // Destroy kills the valyala/fasthttp session and remove the associated cookie 116 | DestroyFasthttp(ctx *fasthttp.RequestCtx) 117 | 118 | // DestroyByID removes the session entry 119 | // from the server-side memory (and database if registered). 120 | // Client's session cookie will still exist but it will be reseted on the next request. 121 | // 122 | // It's safe to use it even if you are not sure if a session with that id exists. 123 | // Works for both net/http & fasthttp 124 | DestroyByID(string) 125 | // DestroyAll removes all sessions 126 | // from the server-side memory (and database if registered). 127 | // Client's session cookie will still exist but it will be reseted on the next request. 128 | // Works for both net/http & fasthttp 129 | DestroyAll() 130 | 131 | // UseDatabase ,optionally, adds a session database to the manager's provider, 132 | // a session db doesn't have write access 133 | // see https://github.com/kataras/go-sessions/tree/master/sessiondb 134 | UseDatabase(Database) 135 | ``` 136 | 137 | ### Configuration 138 | 139 | ```go 140 | // Config is the configuration for sessions. Please read it before using sessions. 141 | Config struct { 142 | // Cookie string, the session's client cookie name, for example: "mysessionid" 143 | // 144 | // Defaults to "irissessionid". 145 | Cookie string 146 | 147 | // CookieSecureTLS set to true if server is running over TLS 148 | // and you need the session's cookie "Secure" field to be setted true. 149 | // 150 | // Note: The user should fill the Decode configuation field in order for this to work. 151 | // Recommendation: You don't need this to be setted to true, just fill the Encode and Decode fields 152 | // with a third-party library like secure cookie, example is provided at the _examples folder. 153 | // 154 | // Defaults to false. 155 | CookieSecureTLS bool 156 | 157 | // AllowReclaim will allow to 158 | // Destroy and Start a session in the same request handler. 159 | // All it does is that it removes the cookie for both `Request` and `ResponseWriter` while `Destroy` 160 | // or add a new cookie to `Request` while `Start`. 161 | // 162 | // Defaults to false. 163 | AllowReclaim bool 164 | 165 | // Encode the cookie value if not nil. 166 | // Should accept as first argument the cookie name (config.Cookie) 167 | // as second argument the server's generated session id. 168 | // Should return the new session id, if error the session id setted to empty which is invalid. 169 | // 170 | // Note: Errors are not printed, so you have to know what you're doing, 171 | // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. 172 | // You either need to provide exactly that amount or you derive the key from what you type in. 173 | // 174 | // Defaults to nil. 175 | Encode func(cookieName string, value interface{}) (string, error) 176 | // Decode the cookie value if not nil. 177 | // Should accept as first argument the cookie name (config.Cookie) 178 | // as second second accepts the client's cookie value (the encoded session id). 179 | // Should return an error if decode operation failed. 180 | // 181 | // Note: Errors are not printed, so you have to know what you're doing, 182 | // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. 183 | // You either need to provide exactly that amount or you derive the key from what you type in. 184 | // 185 | // Defaults to nil. 186 | Decode func(cookieName string, cookieValue string, v interface{}) error 187 | 188 | // Encoding same as Encode and Decode but receives a single instance which 189 | // completes the "CookieEncoder" interface, `Encode` and `Decode` functions. 190 | // 191 | // Defaults to nil. 192 | Encoding Encoding 193 | 194 | // Expires the duration of which the cookie must expires (created_time.Add(Expires)). 195 | // If you want to delete the cookie when the browser closes, set it to -1. 196 | // 197 | // 0 means no expire, (24 years) 198 | // -1 means when browser closes 199 | // > 0 is the time.Duration which the session cookies should expire. 200 | // 201 | // Defaults to infinitive/unlimited life duration(0). 202 | Expires time.Duration 203 | 204 | // SessionIDGenerator should returns a random session id. 205 | // By default we will use a uuid impl package to generate 206 | // that, but developers can change that with simple assignment. 207 | SessionIDGenerator func() string 208 | 209 | // DisableSubdomainPersistence set it to true in order dissallow your subdomains to have access to the session cookie 210 | // 211 | // Defaults to false. 212 | DisableSubdomainPersistence bool 213 | } 214 | ``` 215 | 216 | 217 | Usage NET/HTTP 218 | ------------ 219 | 220 | 221 | `Start` returns a `Session`, **Session outline** 222 | 223 | ```go 224 | ID() string 225 | Get(string) interface{} 226 | HasFlash() bool 227 | GetFlash(string) interface{} 228 | GetString(key string) string 229 | GetFlashString(string) string 230 | GetInt(key string) (int, error) 231 | GetInt64(key string) (int64, error) 232 | GetFloat32(key string) (float32, error) 233 | GetFloat64(key string) (float64, error) 234 | GetBoolean(key string) (bool, error) 235 | GetAll() map[string]interface{} 236 | GetFlashes() map[string]interface{} 237 | VisitAll(cb func(k string, v interface{})) 238 | Set(string, interface{}) 239 | SetImmutable(key string, value interface{}) 240 | SetFlash(string, interface{}) 241 | Delete(string) 242 | Clear() 243 | ClearFlashes() 244 | ``` 245 | 246 | ```go 247 | package main 248 | 249 | import ( 250 | "fmt" 251 | "net/http" 252 | "time" 253 | 254 | "github.com/kataras/go-sessions/v3" 255 | ) 256 | 257 | type businessModel struct { 258 | Name string 259 | } 260 | 261 | func main() { 262 | app := http.NewServeMux() 263 | sess := sessions.New(sessions.Config{ 264 | // Cookie string, the session's client cookie name, for example: "mysessionid" 265 | // 266 | // Defaults to "gosessionid" 267 | Cookie: "mysessionid", 268 | // it's time.Duration, from the time cookie is created, how long it can be alive? 269 | // 0 means no expire. 270 | // -1 means expire when browser closes 271 | // or set a value, like 2 hours: 272 | Expires: time.Hour * 2, 273 | // if you want to invalid cookies on different subdomains 274 | // of the same host, then enable it 275 | DisableSubdomainPersistence: false, 276 | // want to be crazy safe? Take a look at the "securecookie" example folder. 277 | }) 278 | 279 | app.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 280 | w.Write([]byte(fmt.Sprintf("You should navigate to the /set, /get, /delete, /clear,/destroy instead"))) 281 | }) 282 | app.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 283 | 284 | //set session values. 285 | s := sess.Start(w, r) 286 | s.Set("name", "iris") 287 | 288 | //test if setted here 289 | w.Write([]byte(fmt.Sprintf("All ok session setted to: %s", s.GetString("name")))) 290 | 291 | // Set will set the value as-it-is, 292 | // if it's a slice or map 293 | // you will be able to change it on .Get directly! 294 | // Keep note that I don't recommend saving big data neither slices or maps on a session 295 | // but if you really need it then use the `SetImmutable` instead of `Set`. 296 | // Use `SetImmutable` consistently, it's slower. 297 | // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 298 | }) 299 | 300 | app.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 301 | // get a specific value, as string, if no found returns just an empty string 302 | name := sess.Start(w, r).GetString("name") 303 | 304 | w.Write([]byte(fmt.Sprintf("The name on the /set was: %s", name))) 305 | }) 306 | 307 | app.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) { 308 | // delete a specific key 309 | sess.Start(w, r).Delete("name") 310 | }) 311 | 312 | app.HandleFunc("/clear", func(w http.ResponseWriter, r *http.Request) { 313 | // removes all entries 314 | sess.Start(w, r).Clear() 315 | }) 316 | 317 | app.HandleFunc("/update", func(w http.ResponseWriter, r *http.Request) { 318 | // updates expire date 319 | sess.ShiftExpiration(w, r) 320 | }) 321 | 322 | app.HandleFunc("/destroy", func(w http.ResponseWriter, r *http.Request) { 323 | 324 | //destroy, removes the entire session data and cookie 325 | sess.Destroy(w, r) 326 | }) 327 | // Note about Destroy: 328 | // 329 | // You can destroy a session outside of a handler too, using the: 330 | // mySessions.DestroyByID 331 | // mySessions.DestroyAll 332 | 333 | // remember: slices and maps are muttable by-design 334 | // The `SetImmutable` makes sure that they will be stored and received 335 | // as immutable, so you can't change them directly by mistake. 336 | // 337 | // Use `SetImmutable` consistently, it's slower than `Set`. 338 | // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 339 | app.HandleFunc("/set_immutable", func(w http.ResponseWriter, r *http.Request) { 340 | business := []businessModel{{Name: "Edward"}, {Name: "value 2"}} 341 | s := sess.Start(w, r) 342 | s.SetImmutable("businessEdit", business) 343 | businessGet := s.Get("businessEdit").([]businessModel) 344 | 345 | // try to change it, if we used `Set` instead of `SetImmutable` this 346 | // change will affect the underline array of the session's value "businessEdit", but now it will not. 347 | businessGet[0].Name = "Gabriel" 348 | 349 | }) 350 | 351 | app.HandleFunc("/get_immutable", func(w http.ResponseWriter, r *http.Request) { 352 | valSlice := sess.Start(w, r).Get("businessEdit") 353 | if valSlice == nil { 354 | w.Header().Set("Content-Type", "text/html; charset=UTF-8") 355 | w.Write([]byte("please navigate to the /set_immutable first")) 356 | return 357 | } 358 | 359 | firstModel := valSlice.([]businessModel)[0] 360 | // businessGet[0].Name is equal to Edward initially 361 | if firstModel.Name != "Edward" { 362 | panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable") 363 | } 364 | 365 | w.Write([]byte(fmt.Sprintf("[]businessModel[0].Name remains: %s", firstModel.Name))) 366 | 367 | // the name should remains "Edward" 368 | }) 369 | 370 | http.ListenAndServe(":8080", app) 371 | } 372 | ``` 373 | 374 | 375 | Usage FASTHTTP 376 | ------------ 377 | 378 | `StartFasthttp` returns the same object as `Start`, `Session`. 379 | 380 | ```go 381 | ID() string 382 | Get(string) interface{} 383 | HasFlash() bool 384 | GetFlash(string) interface{} 385 | GetString(key string) string 386 | GetFlashString(string) string 387 | GetInt(key string) (int, error) 388 | GetInt64(key string) (int64, error) 389 | GetFloat32(key string) (float32, error) 390 | GetFloat64(key string) (float64, error) 391 | GetBoolean(key string) (bool, error) 392 | GetAll() map[string]interface{} 393 | GetFlashes() map[string]interface{} 394 | VisitAll(cb func(k string, v interface{})) 395 | Set(string, interface{}) 396 | SetImmutable(key string, value interface{}) 397 | SetFlash(string, interface{}) 398 | Delete(string) 399 | Clear() 400 | ClearFlashes() 401 | ``` 402 | 403 | We have only one simple example because the API is the same, the returned session is the same for both net/http and valyala/fasthttp. 404 | 405 | Just append the word "Fasthttp", the rest of the API remains as it's with net/http. 406 | 407 | `Start` for net/http, `StartFasthttp` for valyala/fasthttp. 408 | `ShiftExpiration` for net/http, `ShiftExpirationFasthttp` for valyala/fasthttp. 409 | `UpdateExpiration` for net/http, `UpdateExpirationFasthttp` for valyala/fasthttp. 410 | `Destroy` for net/http, `DestroyFasthttp` for valyala/fasthttp. 411 | 412 | ```go 413 | package main 414 | 415 | import ( 416 | "fmt" 417 | 418 | "github.com/kataras/go-sessions/v3" 419 | "github.com/valyala/fasthttp" 420 | ) 421 | 422 | func main() { 423 | // set some values to the session 424 | setHandler := func(reqCtx *fasthttp.RequestCtx) { 425 | values := map[string]interface{}{ 426 | "Name": "go-sessions", 427 | "Days": "1", 428 | "Secret": "dsads£2132215£%%Ssdsa", 429 | } 430 | 431 | sess := sessions.StartFasthttp(reqCtx) // init the session 432 | // sessions.StartFasthttp returns the, same, Session interface we saw before too 433 | 434 | for k, v := range values { 435 | sess.Set(k, v) // fill session, set each of the key-value pair 436 | } 437 | reqCtx.WriteString("Session saved, go to /get to view the results") 438 | } 439 | 440 | // get the values from the session 441 | getHandler := func(reqCtx *fasthttp.RequestCtx) { 442 | sess := sessions.StartFasthttp(reqCtx) // init the session 443 | sessValues := sess.GetAll() // get all values from this session 444 | 445 | reqCtx.WriteString(fmt.Sprintf("%#v", sessValues)) 446 | } 447 | 448 | // clear all values from the session 449 | clearHandler := func(reqCtx *fasthttp.RequestCtx) { 450 | sess := sessions.StartFasthttp(reqCtx) 451 | sess.Clear() 452 | } 453 | 454 | // destroys the session, clears the values and removes the server-side entry and client-side sessionid cookie 455 | destroyHandler := func(reqCtx *fasthttp.RequestCtx) { 456 | sessions.DestroyFasthttp(reqCtx) 457 | } 458 | 459 | fmt.Println("Open a browser tab and navigate to the localhost:8080/set") 460 | fasthttp.ListenAndServe(":8080", func(reqCtx *fasthttp.RequestCtx) { 461 | path := string(reqCtx.Path()) 462 | 463 | if path == "/set" { 464 | setHandler(reqCtx) 465 | } else if path == "/get" { 466 | getHandler(reqCtx) 467 | } else if path == "/clear" { 468 | clearHandler(reqCtx) 469 | } else if path == "/destroy" { 470 | destroyHandler(reqCtx) 471 | } else { 472 | reqCtx.WriteString("Please navigate to /set or /get or /clear or /destroy") 473 | } 474 | }) 475 | } 476 | ``` 477 | 478 | FAQ 479 | ------------ 480 | 481 | If you'd like to discuss this package, or ask questions about it, feel free to 482 | 483 | * Explore [these questions](https://github.com/kataras/go-sessions/issues?go-sessions=label%3Aquestion). 484 | * Post an issue or idea [here](https://github.com/kataras/go-sessions/issues). 485 | * Navigate to the [Chat][Chat]. 486 | 487 | 488 | 489 | Versioning 490 | ------------ 491 | 492 | Current: **v3.3.0** 493 | 494 | Read more about Semantic Versioning 2.0.0 495 | 496 | - http://semver.org/ 497 | - https://en.wikipedia.org/wiki/Software_versioning 498 | - https://wiki.debian.org/UpstreamGuide#Releases_and_Versions 499 | 500 | People 501 | ------------ 502 | 503 | The author of go-sessions is [@kataras](https://github.com/kataras). 504 | 505 | Contributing 506 | ------------ 507 | 508 | If you are interested in contributing to the go-sessions project, please make a PR. 509 | 510 | License 511 | ------------ 512 | 513 | This project is licensed under the MIT License. 514 | 515 | License can be found [here](LICENSE). 516 | 517 | [Travis Widget]: https://img.shields.io/travis/kataras/go-sessions.svg?style=flat-square 518 | [Travis]: http://travis-ci.org/kataras/go-sessions 519 | [License Widget]: https://img.shields.io/badge/license-MIT%20%20License%20-E91E63.svg?style=flat-square 520 | [License]: https://github.com/kataras/go-sessions/blob/master/LICENSE 521 | [Release Widget]: https://img.shields.io/badge/release-v3.3.0-blue.svg?style=flat-square 522 | [Release]: https://github.com/kataras/go-sessions/releases 523 | [Chat Widget]: https://img.shields.io/badge/community-chat-00BCD4.svg?style=flat-square 524 | [Chat]: https://kataras.rocket.chat/channel/go-sessions 525 | [ChatMain]: https://kataras.rocket.chat/channel/go-sessions 526 | [ChatAlternative]: https://gitter.im/kataras/go-sessions 527 | [Report Widget]: https://img.shields.io/badge/report%20card-A%2B-F44336.svg?style=flat-square 528 | [Report]: http://goreportcard.com/report/kataras/go-sessions 529 | [Documentation Widget]: https://img.shields.io/badge/docs-reference-5272B4.svg?style=flat-square 530 | [Documentation]: https://godoc.org/github.com/kataras/go-sessions 531 | [Language Widget]: https://img.shields.io/badge/powered_by-Go-3362c2.svg?style=flat-square 532 | [Language]: http://golang.org 533 | [Platform Widget]: https://img.shields.io/badge/platform-Any--OS-yellow.svg?style=flat-square 534 | -------------------------------------------------------------------------------- /_examples/database/redis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/go-sessions/v3" 8 | "github.com/kataras/go-sessions/v3/sessiondb/redis" 9 | "github.com/kataras/go-sessions/v3/sessiondb/redis/service" 10 | ) 11 | 12 | func main() { 13 | // replace with your running redis' server settings: 14 | db := redis.New(service.Config{Network: service.DefaultRedisNetwork, 15 | Addr: service.DefaultRedisAddr, 16 | Password: "", 17 | Database: "", 18 | MaxIdle: 0, 19 | MaxActive: 0, 20 | IdleTimeout: service.DefaultRedisIdleTimeout, 21 | Prefix: "", 22 | }) // to use badger just use the sessiondb/badger#New func. 23 | 24 | defer db.Close() 25 | 26 | sess := sessions.New(sessions.Config{Cookie: "sessionscookieid"}) 27 | 28 | // 29 | // IMPORTANT: 30 | // 31 | sess.UseDatabase(db) 32 | 33 | // the rest of the code stays the same. 34 | app := http.NewServeMux() 35 | 36 | app.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 37 | w.Write([]byte(fmt.Sprintf("You should navigate to the /set, /get, /delete, /clear,/destroy instead"))) 38 | }) 39 | app.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 40 | s := sess.Start(w, r) 41 | //set session values 42 | s.Set("name", "iris") 43 | 44 | //test if setted here 45 | w.Write([]byte(fmt.Sprintf("All ok session setted to: %s", s.GetString("name")))) 46 | }) 47 | 48 | app.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 49 | // get a specific key, as string, if no found returns just an empty string 50 | name := sess.Start(w, r).GetString("name") 51 | 52 | w.Write([]byte(fmt.Sprintf("The name on the /set was: %s", name))) 53 | }) 54 | 55 | app.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) { 56 | // delete a specific key 57 | sess.Start(w, r).Delete("name") 58 | }) 59 | 60 | app.HandleFunc("/clear", func(w http.ResponseWriter, r *http.Request) { 61 | // removes all entries 62 | sess.Start(w, r).Clear() 63 | }) 64 | 65 | app.HandleFunc("/destroy", func(w http.ResponseWriter, r *http.Request) { 66 | //destroy, removes the entire session data and cookie 67 | sess.Destroy(w, r) 68 | }) 69 | 70 | app.HandleFunc("/update", func(w http.ResponseWriter, r *http.Request) { 71 | // updates expire date with a new date 72 | sess.ShiftExpiration(w, r) 73 | }) 74 | 75 | http.ListenAndServe(":8080", app) 76 | } 77 | -------------------------------------------------------------------------------- /_examples/database/rediscluster/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/kataras/go-sessions/v3" 9 | "github.com/kataras/go-sessions/v3/sessiondb/rediscluster" 10 | "github.com/kataras/go-sessions/v3/sessiondb/rediscluster/service" 11 | ) 12 | 13 | func main() { 14 | // replace with your running redis' server settings: 15 | db := rediscluster.New(service.Config{Network: service.DefaultRedisNetwork, 16 | Addr: "k8s-istiosys-chtdevba-88ea655fc7-8f783365b293175a.elb.ap-southeast-1.amazonaws.com:6380", 17 | Password: "redis-auth", 18 | Database: "", 19 | MaxIdle: 0, 20 | MaxActive: 0, 21 | IdleTimeout: service.DefaultRedisIdleTimeout, 22 | Prefix: "", 23 | }) // to use badger just use the sessiondb/badger#New func. 24 | 25 | defer db.Close() 26 | 27 | sess := sessions.New(sessions.Config{Cookie: "sessionscookieid", Expires: 5 * time.Second}) 28 | 29 | // 30 | // IMPORTANT: 31 | // 32 | sess.UseDatabase(db) 33 | 34 | // the rest of the code stays the same. 35 | app := http.NewServeMux() 36 | 37 | app.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 38 | w.Write([]byte(fmt.Sprintf("You should navigate to the /set, /get, /delete, /clear,/destroy instead"))) 39 | }) 40 | app.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 41 | s := sess.Start(w, r) 42 | // set session values 43 | s.Set("name", "iris") 44 | 45 | fmt.Println(s.Get("name")) 46 | 47 | // test if setted here 48 | w.Write([]byte(fmt.Sprintf("All ok session setted to: %s", s.GetString("name")))) 49 | }) 50 | 51 | app.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 52 | // get a specific key, as string, if no found returns just an empty string 53 | name := sess.Start(w, r).GetString("name") 54 | 55 | w.Write([]byte(fmt.Sprintf("The name on the /set was: %s", name))) 56 | }) 57 | 58 | app.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) { 59 | // delete a specific key 60 | sess.Start(w, r).Delete("name") 61 | }) 62 | 63 | app.HandleFunc("/clear", func(w http.ResponseWriter, r *http.Request) { 64 | // removes all entries 65 | sess.Start(w, r).Clear() 66 | }) 67 | 68 | app.HandleFunc("/destroy", func(w http.ResponseWriter, r *http.Request) { 69 | // destroy, removes the entire session data and cookie 70 | sess.Destroy(w, r) 71 | }) 72 | 73 | app.HandleFunc("/update", func(w http.ResponseWriter, r *http.Request) { 74 | // updates expire date with a new date 75 | sess.ShiftExpiration(w, r) 76 | }) 77 | 78 | http.ListenAndServe(":8080", app) 79 | } 80 | -------------------------------------------------------------------------------- /_examples/fasthttp/README.md: -------------------------------------------------------------------------------- 1 | # Fasthttp example 2 | 3 | We have only one simple example because the API is the same, the returned session is the same for both net/http and valyala/fasthttp. 4 | 5 | ## Just append the word "Fasthttp", the rest of the API remains as it's with net/http 6 | 7 | `Start` for net/http, `StartFasthttp` for valyala/fasthttp. 8 | `ShiftExpiration` for net/http, `ShiftExpirationFasthttp` for valyala/fasthttp. 9 | `UpdateExpiration` for net/http, `UpdateExpirationFasthttp` for valyala/fasthttp. 10 | `Destroy` for net/http, `DestroyFasthttp` for valyala/fasthttp. -------------------------------------------------------------------------------- /_examples/fasthttp/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/kataras/go-sessions/v3" 7 | "github.com/valyala/fasthttp" 8 | ) 9 | 10 | func main() { 11 | 12 | // set some values to the session 13 | setHandler := func(reqCtx *fasthttp.RequestCtx) { 14 | values := map[string]interface{}{ 15 | "Name": "go-sessions", 16 | "Days": "1", 17 | "Secret": "dsads£2132215£%%Ssdsa", 18 | } 19 | 20 | sess := sessions.StartFasthttp(reqCtx) // init the session 21 | // sessions.StartFasthttp returns the, same, Session interface we saw before too 22 | 23 | for k, v := range values { 24 | sess.Set(k, v) // fill session, set each of the key-value pair 25 | } 26 | reqCtx.WriteString("Session saved, go to /get to view the results") 27 | } 28 | 29 | // get the values from the session 30 | getHandler := func(reqCtx *fasthttp.RequestCtx) { 31 | sess := sessions.StartFasthttp(reqCtx) // init the session 32 | sessValues := sess.GetAll() // get all values from this session 33 | 34 | reqCtx.WriteString(fmt.Sprintf("%#v", sessValues)) 35 | } 36 | 37 | // clear all values from the session 38 | clearHandler := func(reqCtx *fasthttp.RequestCtx) { 39 | sess := sessions.StartFasthttp(reqCtx) 40 | sess.Clear() 41 | } 42 | 43 | // destroys the session, clears the values and removes the server-side entry and client-side sessionid cookie 44 | destroyHandler := func(reqCtx *fasthttp.RequestCtx) { 45 | sessions.DestroyFasthttp(reqCtx) 46 | } 47 | 48 | fmt.Println("Open a browser tab and navigate to the localhost:8080/set") 49 | fasthttp.ListenAndServe(":8080", func(reqCtx *fasthttp.RequestCtx) { 50 | path := string(reqCtx.Path()) 51 | 52 | if path == "/set" { 53 | setHandler(reqCtx) 54 | } else if path == "/get" { 55 | getHandler(reqCtx) 56 | } else if path == "/clear" { 57 | clearHandler(reqCtx) 58 | } else if path == "/destroy" { 59 | destroyHandler(reqCtx) 60 | } else { 61 | reqCtx.WriteString("Please navigate to /set or /get or /clear or /destroy") 62 | } 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /_examples/flash-messages/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/kataras/go-sessions/v3" 8 | ) 9 | 10 | func main() { 11 | app := http.NewServeMux() 12 | sess := sessions.New(sessions.Config{Cookie: "myappsessionid"}) 13 | 14 | app.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 15 | s := sess.Start(w, r) 16 | s.SetFlash("name", "iris") 17 | w.Write([]byte(fmt.Sprintf("Message setted, is available for the next request"))) 18 | }) 19 | 20 | app.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 21 | s := sess.Start(w, r) 22 | name := s.GetFlashString("name") 23 | if name == "" { 24 | w.Write([]byte(fmt.Sprintf("Empty name!!"))) 25 | return 26 | } 27 | w.Write([]byte(fmt.Sprintf("Hello %s", name))) 28 | }) 29 | 30 | app.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { 31 | s := sess.Start(w, r) 32 | name := s.GetFlashString("name") 33 | if name == "" { 34 | w.Write([]byte(fmt.Sprintf("Empty name!!"))) 35 | return 36 | } 37 | 38 | w.Write([]byte(fmt.Sprintf("Ok you are coming from /set ,the value of the name is %s", name))) 39 | w.Write([]byte(fmt.Sprintf(", and again from the same context: %s", name))) 40 | }) 41 | 42 | http.ListenAndServe(":8080", app) 43 | } 44 | -------------------------------------------------------------------------------- /_examples/overview/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/kataras/go-sessions/v3" 7 | ) 8 | 9 | var ( 10 | cookieNameForSessionID = "mycookiesessionnameid" 11 | sess = sessions.New(sessions.Config{Cookie: cookieNameForSessionID}) 12 | ) 13 | 14 | func secret(w http.ResponseWriter, r *http.Request) { 15 | 16 | // Check if user is authenticated 17 | if auth, _ := sess.Start(w, r).GetBoolean("authenticated"); !auth { 18 | w.WriteHeader(http.StatusForbidden) 19 | return 20 | } 21 | 22 | // Print secret message 23 | w.Write([]byte("The cake is a lie!")) 24 | } 25 | 26 | func login(w http.ResponseWriter, r *http.Request) { 27 | session := sess.Start(w, r) 28 | 29 | // Authentication goes here 30 | // ... 31 | 32 | // Set user as authenticated 33 | session.Set("authenticated", true) 34 | } 35 | 36 | func logout(w http.ResponseWriter, r *http.Request) { 37 | session := sess.Start(w, r) 38 | 39 | // Revoke users authentication 40 | session.Set("authenticated", false) 41 | } 42 | 43 | func main() { 44 | app := http.NewServeMux() 45 | 46 | app.HandleFunc("/secret", secret) 47 | app.HandleFunc("/login", login) 48 | app.HandleFunc("/logout", logout) 49 | 50 | http.ListenAndServe(":8080", app) 51 | } 52 | -------------------------------------------------------------------------------- /_examples/securecookie/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // developers can use any library to add a custom cookie encoder/decoder. 4 | // At this example we use the gorilla's securecookie package: 5 | // $ go get github.com/gorilla/securecookie 6 | // $ go run main.go 7 | 8 | import ( 9 | "fmt" 10 | "net/http" 11 | 12 | "github.com/gorilla/securecookie" 13 | "github.com/kataras/go-sessions/v3" 14 | ) 15 | 16 | func main() { 17 | app := http.NewServeMux() 18 | 19 | cookieName := "mycustomsessionid" 20 | // AES only supports key sizes of 16, 24 or 32 bytes. 21 | // You either need to provide exactly that amount or you derive the key from what you type in. 22 | hashKey := []byte("the-big-and-secret-fash-key-here") 23 | blockKey := []byte("lot-secret-of-characters-big-too") 24 | secureCookie := securecookie.New(hashKey, blockKey) 25 | 26 | mySessions := sessions.New(sessions.Config{ 27 | Cookie: cookieName, 28 | Encode: secureCookie.Encode, 29 | Decode: secureCookie.Decode, 30 | }) 31 | 32 | app.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 33 | w.Write([]byte(fmt.Sprintf("You should navigate to the /set, /get, /delete, /clear,/destroy instead"))) 34 | }) 35 | 36 | app.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 37 | //set session values 38 | s := mySessions.Start(w, r) 39 | s.Set("name", "iris") 40 | 41 | //test if setted here 42 | w.Write([]byte(fmt.Sprintf("All ok session setted to: %s", s.GetString("name")))) 43 | }) 44 | 45 | app.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 46 | // get a specific key, as string, if no found returns just an empty string 47 | s := mySessions.Start(w, r) 48 | name := s.GetString("name") 49 | 50 | w.Write([]byte(fmt.Sprintf("The name on the /set was: %s", name))) 51 | }) 52 | 53 | app.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) { 54 | // delete a specific key 55 | s := mySessions.Start(w, r) 56 | s.Delete("name") 57 | }) 58 | 59 | app.HandleFunc("/clear", func(w http.ResponseWriter, r *http.Request) { 60 | // removes all entries 61 | mySessions.Start(w, r).Clear() 62 | }) 63 | 64 | app.HandleFunc("/update", func(w http.ResponseWriter, r *http.Request) { 65 | // updates expire date with a new date 66 | mySessions.ShiftExpiration(w, r) 67 | }) 68 | 69 | app.HandleFunc("/destroy", func(w http.ResponseWriter, r *http.Request) { 70 | //destroy, removes the entire session data and cookie 71 | mySessions.Destroy(w, r) 72 | }) 73 | http.ListenAndServe(":8080", app) 74 | } 75 | -------------------------------------------------------------------------------- /_examples/standalone/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/kataras/go-sessions/v3" 9 | ) 10 | 11 | type businessModel struct { 12 | Name string 13 | } 14 | 15 | func main() { 16 | app := http.NewServeMux() 17 | sess := sessions.New(sessions.Config{ 18 | // Cookie string, the session's client cookie name, for example: "mysessionid" 19 | // 20 | // Defaults to "gosessionid" 21 | Cookie: "mysessionid", 22 | // it's time.Duration, from the time cookie is created, how long it can be alive? 23 | // 0 means no expire. 24 | // -1 means expire when browser closes 25 | // or set a value, like 2 hours: 26 | Expires: time.Hour * 2, 27 | // if you want to invalid cookies on different subdomains 28 | // of the same host, then enable it 29 | DisableSubdomainPersistence: false, 30 | // want to be crazy safe? Take a look at the "securecookie" example folder. 31 | }) 32 | 33 | app.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 34 | w.Write([]byte(fmt.Sprintf("You should navigate to the /set, /get, /delete, /clear,/destroy instead"))) 35 | }) 36 | app.HandleFunc("/set", func(w http.ResponseWriter, r *http.Request) { 37 | 38 | //set session values. 39 | s := sess.Start(w, r) 40 | s.Set("name", "iris") 41 | 42 | //test if setted here 43 | w.Write([]byte(fmt.Sprintf("All ok session setted to: %s", s.GetString("name")))) 44 | 45 | // Set will set the value as-it-is, 46 | // if it's a slice or map 47 | // you will be able to change it on .Get directly! 48 | // Keep note that I don't recommend saving big data neither slices or maps on a session 49 | // but if you really need it then use the `SetImmutable` instead of `Set`. 50 | // Use `SetImmutable` consistently, it's slower. 51 | // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 52 | }) 53 | 54 | app.HandleFunc("/get", func(w http.ResponseWriter, r *http.Request) { 55 | // get a specific value, as string, if no found returns just an empty string 56 | name := sess.Start(w, r).GetString("name") 57 | 58 | w.Write([]byte(fmt.Sprintf("The name on the /set was: %s", name))) 59 | }) 60 | 61 | app.HandleFunc("/delete", func(w http.ResponseWriter, r *http.Request) { 62 | // delete a specific key 63 | sess.Start(w, r).Delete("name") 64 | }) 65 | 66 | app.HandleFunc("/clear", func(w http.ResponseWriter, r *http.Request) { 67 | // removes all entries 68 | sess.Start(w, r).Clear() 69 | }) 70 | 71 | app.HandleFunc("/update", func(w http.ResponseWriter, r *http.Request) { 72 | // updates expire date 73 | sess.ShiftExpiration(w, r) 74 | }) 75 | 76 | app.HandleFunc("/destroy", func(w http.ResponseWriter, r *http.Request) { 77 | 78 | //destroy, removes the entire session data and cookie 79 | sess.Destroy(w, r) 80 | }) 81 | // Note about Destroy: 82 | // 83 | // You can destroy a session outside of a handler too, using the: 84 | // mySessions.DestroyByID 85 | // mySessions.DestroyAll 86 | 87 | // remember: slices and maps are muttable by-design 88 | // The `SetImmutable` makes sure that they will be stored and received 89 | // as immutable, so you can't change them directly by mistake. 90 | // 91 | // Use `SetImmutable` consistently, it's slower than `Set`. 92 | // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 93 | app.HandleFunc("/set_immutable", func(w http.ResponseWriter, r *http.Request) { 94 | business := []businessModel{{Name: "Edward"}, {Name: "value 2"}} 95 | s := sess.Start(w, r) 96 | s.SetImmutable("businessEdit", business) 97 | businessGet := s.Get("businessEdit").([]businessModel) 98 | 99 | // try to change it, if we used `Set` instead of `SetImmutable` this 100 | // change will affect the underline array of the session's value "businessEdit", but now it will not. 101 | businessGet[0].Name = "Gabriel" 102 | 103 | }) 104 | 105 | app.HandleFunc("/get_immutable", func(w http.ResponseWriter, r *http.Request) { 106 | valSlice := sess.Start(w, r).Get("businessEdit") 107 | if valSlice == nil { 108 | w.Header().Set("Content-Type", "text/html; charset=UTF-8") 109 | w.Write([]byte("please navigate to the /set_immutable first")) 110 | return 111 | } 112 | 113 | firstModel := valSlice.([]businessModel)[0] 114 | // businessGet[0].Name is equal to Edward initially 115 | if firstModel.Name != "Edward" { 116 | panic("Report this as a bug, immutable data cannot be changed from the caller without re-SetImmutable") 117 | } 118 | 119 | w.Write([]byte(fmt.Sprintf("[]businessModel[0].Name remains: %s", firstModel.Name))) 120 | 121 | // the name should remains "Edward" 122 | }) 123 | 124 | http.ListenAndServe(":8080", app) 125 | } 126 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | const ( 10 | // DefaultCookieName the secret cookie's name for sessions 11 | DefaultCookieName = "gosessionid" 12 | ) 13 | 14 | // Encoding is the Cookie Encoder/Decoder interface, which can be passed as configuration field 15 | // alternatively to the `Encode` and `Decode` fields. 16 | type Encoding interface { 17 | // Encode the cookie value if not nil. 18 | // Should accept as first argument the cookie name (config.Name) 19 | // as second argument the server's generated session id. 20 | // Should return the new session id, if error the session id setted to empty which is invalid. 21 | // 22 | // Note: Errors are not printed, so you have to know what you're doing, 23 | // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. 24 | // You either need to provide exactly that amount or you derive the key from what you type in. 25 | // 26 | // Defaults to nil 27 | Encode(cookieName string, value interface{}) (string, error) 28 | // Decode the cookie value if not nil. 29 | // Should accept as first argument the cookie name (config.Name) 30 | // as second second accepts the client's cookie value (the encoded session id). 31 | // Should return an error if decode operation failed. 32 | // 33 | // Note: Errors are not printed, so you have to know what you're doing, 34 | // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. 35 | // You either need to provide exactly that amount or you derive the key from what you type in. 36 | // 37 | // Defaults to nil 38 | Decode(cookieName string, cookieValue string, v interface{}) error 39 | } 40 | 41 | type ( 42 | // Config is the configuration for sessions. Please read it before using sessions. 43 | Config struct { 44 | // Cookie string, the session's client cookie name, for example: "mysessionid" 45 | // 46 | // Defaults to "irissessionid". 47 | Cookie string 48 | 49 | // CookieSecureTLS set to true if server is running over TLS 50 | // and you need the session's cookie "Secure" field to be setted true. 51 | // 52 | // Note: The user should fill the Decode configuation field in order for this to work. 53 | // Recommendation: You don't need this to be setted to true, just fill the Encode and Decode fields 54 | // with a third-party library like secure cookie, example is provided at the _examples folder. 55 | // 56 | // Defaults to false. 57 | CookieSecureTLS bool 58 | 59 | // AllowReclaim will allow to 60 | // Destroy and Start a session in the same request handler. 61 | // All it does is that it removes the cookie for both `Request` and `ResponseWriter` while `Destroy` 62 | // or add a new cookie to `Request` while `Start`. 63 | // 64 | // Defaults to false. 65 | AllowReclaim bool 66 | 67 | // Encode the cookie value if not nil. 68 | // Should accept as first argument the cookie name (config.Cookie) 69 | // as second argument the server's generated session id. 70 | // Should return the new session id, if error the session id setted to empty which is invalid. 71 | // 72 | // Note: Errors are not printed, so you have to know what you're doing, 73 | // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. 74 | // You either need to provide exactly that amount or you derive the key from what you type in. 75 | // 76 | // Defaults to nil. 77 | Encode func(cookieName string, value interface{}) (string, error) 78 | // Decode the cookie value if not nil. 79 | // Should accept as first argument the cookie name (config.Cookie) 80 | // as second second accepts the client's cookie value (the encoded session id). 81 | // Should return an error if decode operation failed. 82 | // 83 | // Note: Errors are not printed, so you have to know what you're doing, 84 | // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes. 85 | // You either need to provide exactly that amount or you derive the key from what you type in. 86 | // 87 | // Defaults to nil. 88 | Decode func(cookieName string, cookieValue string, v interface{}) error 89 | 90 | // Encoding same as Encode and Decode but receives a single instance which 91 | // completes the "CookieEncoder" interface, `Encode` and `Decode` functions. 92 | // 93 | // Defaults to nil. 94 | Encoding Encoding 95 | 96 | // Expires the duration of which the cookie must expires (created_time.Add(Expires)). 97 | // If you want to delete the cookie when the browser closes, set it to -1. 98 | // 99 | // 0 means no expire, (24 years) 100 | // -1 means when browser closes 101 | // > 0 is the time.Duration which the session cookies should expire. 102 | // 103 | // Defaults to infinitive/unlimited life duration(0). 104 | Expires time.Duration 105 | 106 | // SessionIDGenerator should returns a random session id. 107 | // By default we will use a uuid impl package to generate 108 | // that, but developers can change that with simple assignment. 109 | SessionIDGenerator func() string 110 | 111 | // DisableSubdomainPersistence set it to true in order dissallow your subdomains to have access to the session cookie 112 | // 113 | // Defaults to false. 114 | DisableSubdomainPersistence bool 115 | } 116 | ) 117 | 118 | // Validate corrects missing fields configuration fields and returns the right configuration 119 | func (c Config) Validate() Config { 120 | 121 | if c.Cookie == "" { 122 | c.Cookie = DefaultCookieName 123 | } 124 | 125 | if c.SessionIDGenerator == nil { 126 | c.SessionIDGenerator = func() string { 127 | id, _ := uuid.NewRandom() 128 | return id.String() 129 | } 130 | } 131 | 132 | if c.Encoding != nil { 133 | c.Encode = c.Encoding.Encode 134 | c.Decode = c.Encoding.Decode 135 | } 136 | 137 | return c 138 | } 139 | -------------------------------------------------------------------------------- /cookie.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | var ( 13 | // CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie. 14 | CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) 15 | 16 | // CookieExpireUnlimited indicates that the cookie doesn't expire. 17 | CookieExpireUnlimited = time.Now().AddDate(24, 10, 10) 18 | ) 19 | 20 | // GetCookie returns cookie's value by it's name 21 | // returns empty string if nothing was found 22 | func GetCookie(r *http.Request, name string) string { 23 | c, err := r.Cookie(name) 24 | if err != nil { 25 | return "" 26 | } 27 | 28 | return c.Value 29 | } 30 | 31 | // GetCookieFasthttp returns cookie's value by it's name 32 | // returns empty string if nothing was found. 33 | func GetCookieFasthttp(ctx *fasthttp.RequestCtx, name string) (value string) { 34 | bcookie := ctx.Request.Header.Cookie(name) 35 | if bcookie != nil { 36 | value = string(bcookie) 37 | } 38 | return 39 | } 40 | 41 | // AddCookie adds a cookie 42 | func AddCookie(w http.ResponseWriter, r *http.Request, cookie *http.Cookie, reclaim bool) { 43 | if reclaim { 44 | r.AddCookie(cookie) 45 | } 46 | http.SetCookie(w, cookie) 47 | } 48 | 49 | // AddCookieFasthttp adds a cookie. 50 | func AddCookieFasthttp(ctx *fasthttp.RequestCtx, cookie *fasthttp.Cookie) { 51 | ctx.Response.Header.SetCookie(cookie) 52 | } 53 | 54 | // RemoveCookie deletes a cookie by it's name/key. 55 | func RemoveCookie(w http.ResponseWriter, r *http.Request, config Config) { 56 | c, err := r.Cookie(config.Cookie) 57 | if err != nil { 58 | return 59 | } 60 | 61 | c.Expires = CookieExpireDelete 62 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 63 | c.MaxAge = -1 64 | c.Value = "" 65 | c.Path = "/" 66 | c.Domain = formatCookieDomain(r.URL.Host, config.DisableSubdomainPersistence) 67 | AddCookie(w, r, c, config.AllowReclaim) 68 | 69 | if config.AllowReclaim { 70 | // delete request's cookie also, which is temporary available. 71 | r.Header.Set("Cookie", "") 72 | } 73 | } 74 | 75 | // RemoveCookieFasthttp deletes a cookie by it's name/key. 76 | func RemoveCookieFasthttp(ctx *fasthttp.RequestCtx, config Config) { 77 | ctx.Response.Header.DelCookie(config.Cookie) 78 | 79 | cookie := fasthttp.AcquireCookie() 80 | //cookie := &fasthttp.Cookie{} 81 | cookie.SetKey(config.Cookie) 82 | cookie.SetValue("") 83 | cookie.SetPath("/") 84 | cookie.SetDomain(formatCookieDomain(string(ctx.Host()), config.DisableSubdomainPersistence)) 85 | cookie.SetHTTPOnly(true) 86 | exp := time.Now().Add(-time.Duration(1) * time.Minute) //RFC says 1 second, but let's do it 1 minute to make sure is working... 87 | cookie.SetExpire(exp) 88 | AddCookieFasthttp(ctx, cookie) 89 | fasthttp.ReleaseCookie(cookie) 90 | // delete request's cookie also, which is temporary available 91 | ctx.Request.Header.DelCookie(config.Cookie) 92 | } 93 | 94 | // IsValidCookieDomain returns true if the receiver is a valid domain to set 95 | // valid means that is recognised as 'domain' by the browser, so it(the cookie) can be shared with subdomains also 96 | func IsValidCookieDomain(domain string) bool { 97 | if domain == "0.0.0.0" || domain == "127.0.0.1" { 98 | // for these type of hosts, we can't allow subdomains persistence, 99 | // the web browser doesn't understand the mysubdomain.0.0.0.0 and mysubdomain.127.0.0.1 mysubdomain.32.196.56.181. as scorrectly ubdomains because of the many dots 100 | // so don't set a cookie domain here, let browser handle this 101 | return false 102 | } 103 | 104 | dotLen := strings.Count(domain, ".") 105 | if dotLen == 0 { 106 | // we don't have a domain, maybe something like 'localhost', browser doesn't see the .localhost as wildcard subdomain+domain 107 | return false 108 | } 109 | if dotLen >= 3 { 110 | if lastDotIdx := strings.LastIndexByte(domain, '.'); lastDotIdx != -1 { 111 | // chekc the last part, if it's number then propably it's ip 112 | if len(domain) > lastDotIdx+1 { 113 | _, err := strconv.Atoi(domain[lastDotIdx+1:]) 114 | if err == nil { 115 | return false 116 | } 117 | } 118 | } 119 | } 120 | 121 | return true 122 | } 123 | 124 | func formatCookieDomain(requestDomain string, disableSubdomainPersistence bool) string { 125 | if disableSubdomainPersistence { 126 | return "" 127 | } 128 | 129 | if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 { 130 | requestDomain = requestDomain[0:portIdx] 131 | } 132 | 133 | if !IsValidCookieDomain(requestDomain) { 134 | return "" 135 | } 136 | 137 | // RFC2109, we allow level 1 subdomains, but no further 138 | // if we have localhost.com , we want the localhost.cos. 139 | // so if we have something like: mysubdomain.localhost.com we want the localhost here 140 | // if we have mysubsubdomain.mysubdomain.localhost.com we want the .mysubdomain.localhost.com here 141 | // slow things here, especially the 'replace' but this is a good and understable( I hope) way to get the be able to set cookies from subdomains & domain with 1-level limit 142 | if dotIdx := strings.LastIndexByte(requestDomain, '.'); dotIdx > 0 { 143 | // is mysubdomain.localhost.com || mysubsubdomain.mysubdomain.localhost.com 144 | s := requestDomain[0:dotIdx] // set mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost 145 | if secondDotIdx := strings.LastIndexByte(s, '.'); secondDotIdx > 0 { 146 | //is mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost 147 | s = s[secondDotIdx+1:] // set to localhost || mysubdomain.localhost 148 | } 149 | // replace the s with the requestDomain before the domain's siffux 150 | subdomainSuff := strings.LastIndexByte(requestDomain, '.') 151 | if subdomainSuff > len(s) { // if it is actual exists as subdomain suffix 152 | requestDomain = strings.Replace(requestDomain, requestDomain[0:subdomainSuff], s, 1) // set to localhost.com || mysubdomain.localhost.com 153 | } 154 | } 155 | // finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow) 156 | return "." + requestDomain // . to allow persistence 157 | } 158 | -------------------------------------------------------------------------------- /database.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // ErrNotImplemented is returned when a particular feature is not yet implemented yet. 10 | // It can be matched directly, i.e: `isNotImplementedError := sessions.ErrNotImplemented.Equal(err)`. 11 | var ErrNotImplemented = errors.New("not implemented yet") 12 | 13 | // Database is the interface which all session databases should implement 14 | // By design it doesn't support any type of cookie session like other frameworks. 15 | // I want to protect you, believe me. 16 | // The scope of the database is to store somewhere the sessions in order to 17 | // keep them after restarting the server, nothing more. 18 | // 19 | // Synchronization are made automatically, you can register one using `UseDatabase`. 20 | // 21 | // Look the `sessiondb` folder for databases implementations. 22 | type Database interface { 23 | // Acquire receives a session's lifetime from the database, 24 | // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. 25 | Acquire(sid string, expires time.Duration) LifeTime 26 | // OnUpdateExpiration should re-set the expiration (ttl) of the session entry inside the database, 27 | // it is fired on `ShiftExpiration` and `UpdateExpiration`. 28 | // If the database does not support change of ttl then the session entry will be cloned to another one 29 | // and the old one will be removed, it depends on the chosen database storage. 30 | // 31 | // Check of error is required, if error returned then the rest session's keys are not proceed. 32 | // 33 | // If a database does not support this feature then an `ErrNotImplemented` will be returned instead. 34 | OnUpdateExpiration(sid string, newExpires time.Duration) error 35 | // Set sets a key value of a specific session. 36 | // The "immutable" input argument depends on the store, it may not implement it at all. 37 | Set(sid string, lifetime LifeTime, key string, value interface{}, immutable bool) 38 | // Get retrieves a session value based on the key. 39 | Get(sid string, key string) interface{} 40 | // Visit loops through all session keys and values. 41 | Visit(sid string, cb func(key string, value interface{})) 42 | // Len returns the length of the session's entries (keys). 43 | Len(sid string) int 44 | // Delete removes a session key value based on its key. 45 | Delete(sid string, key string) (deleted bool) 46 | // Clear removes all session key values but it keeps the session entry. 47 | Clear(sid string) 48 | // Release destroys the session, it clears and removes the session entry, 49 | // session manager will create a new session ID on the next request after this call. 50 | Release(sid string) 51 | } 52 | 53 | type mem struct { 54 | values map[string]*Store 55 | mu sync.RWMutex 56 | } 57 | 58 | var _ Database = (*mem)(nil) 59 | 60 | func newMemDB() Database { return &mem{values: make(map[string]*Store)} } 61 | 62 | func (s *mem) Acquire(sid string, expires time.Duration) LifeTime { 63 | s.mu.Lock() 64 | s.values[sid] = new(Store) 65 | s.mu.Unlock() 66 | return LifeTime{} 67 | } 68 | 69 | // Do nothing, the `LifeTime` of the Session will be managed by the callers automatically on memory-based storage. 70 | func (s *mem) OnUpdateExpiration(string, time.Duration) error { return nil } 71 | 72 | // immutable depends on the store, it may not implement it at all. 73 | func (s *mem) Set(sid string, lifetime LifeTime, key string, value interface{}, immutable bool) { 74 | s.mu.RLock() 75 | s.values[sid].Save(key, value, immutable) 76 | s.mu.RUnlock() 77 | } 78 | 79 | func (s *mem) Get(sid string, key string) interface{} { 80 | s.mu.RLock() 81 | v := s.values[sid].Get(key) 82 | s.mu.RUnlock() 83 | 84 | return v 85 | } 86 | 87 | func (s *mem) Visit(sid string, cb func(key string, value interface{})) { 88 | s.values[sid].Visit(cb) 89 | } 90 | 91 | func (s *mem) Len(sid string) int { 92 | s.mu.RLock() 93 | n := s.values[sid].Len() 94 | s.mu.RUnlock() 95 | 96 | return n 97 | } 98 | 99 | func (s *mem) Delete(sid string, key string) (deleted bool) { 100 | s.mu.RLock() 101 | deleted = s.values[sid].Remove(key) 102 | s.mu.RUnlock() 103 | return 104 | } 105 | 106 | func (s *mem) Clear(sid string) { 107 | s.mu.Lock() 108 | s.values[sid].Reset() 109 | s.mu.Unlock() 110 | } 111 | 112 | func (s *mem) Release(sid string) { 113 | s.mu.Lock() 114 | delete(s.values, sid) 115 | s.mu.Unlock() 116 | } 117 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kataras/go-sessions/v3 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/dgraph-io/badger v1.6.2 7 | github.com/gavv/httpexpect v2.0.0+incompatible 8 | github.com/gomodule/redigo v1.8.9 9 | github.com/google/uuid v1.3.0 10 | github.com/gorilla/securecookie v1.1.1 11 | github.com/mna/redisc v1.3.2 12 | github.com/valyala/fasthttp v1.48.0 13 | go.etcd.io/bbolt v1.3.7 14 | ) 15 | 16 | require ( 17 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect 18 | github.com/ajg/form v1.5.1 // indirect 19 | github.com/andybalholm/brotli v1.0.5 // indirect 20 | github.com/cespare/xxhash v1.1.0 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/dgraph-io/ristretto v0.0.2 // indirect 23 | github.com/dustin/go-humanize v1.0.0 // indirect 24 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 // indirect 25 | github.com/fatih/structs v1.1.0 // indirect 26 | github.com/golang/protobuf v1.5.2 // indirect 27 | github.com/google/go-cmp v0.5.8 // indirect 28 | github.com/google/go-querystring v1.1.0 // indirect 29 | github.com/gorilla/websocket v1.5.0 // indirect 30 | github.com/imkira/go-interpol v1.1.0 // indirect 31 | github.com/klauspost/compress v1.16.3 // indirect 32 | github.com/mattn/go-colorable v0.1.13 // indirect 33 | github.com/moul/http2curl v1.0.0 // indirect 34 | github.com/onsi/ginkgo v1.16.5 // indirect 35 | github.com/onsi/gomega v1.20.1 // indirect 36 | github.com/pkg/errors v0.8.1 // indirect 37 | github.com/pmezard/go-difflib v1.0.0 // indirect 38 | github.com/sergi/go-diff v1.2.0 // indirect 39 | github.com/smartystreets/goconvey v1.7.2 // indirect 40 | github.com/stretchr/testify v1.8.1 // indirect 41 | github.com/valyala/bytebufferpool v1.0.0 // indirect 42 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 43 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 44 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 45 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect 46 | github.com/yudai/gojsondiff v1.0.0 // indirect 47 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect 48 | github.com/yudai/pp v2.0.1+incompatible // indirect 49 | golang.org/x/net v0.8.0 // indirect 50 | golang.org/x/sys v0.6.0 // indirect 51 | google.golang.org/protobuf v1.28.0 // indirect 52 | gopkg.in/yaml.v3 v3.0.1 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= 2 | github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= 3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 4 | github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= 5 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 6 | github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= 7 | github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 8 | github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 9 | github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 10 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 11 | github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= 12 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 13 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 14 | github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= 15 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 16 | github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 19 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 20 | github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= 21 | github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= 22 | github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po= 23 | github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= 24 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= 25 | github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 26 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 27 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 28 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072 h1:DddqAaWDpywytcG8w/qoQ5sAN8X12d3Z3koB0C3Rxsc= 29 | github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= 30 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 31 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 32 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 33 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 34 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 35 | github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= 36 | github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= 37 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 38 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 39 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 40 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 41 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 42 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 43 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 44 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 45 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 46 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 47 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 48 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 49 | github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= 50 | github.com/gomodule/redigo v1.8.9 h1:Sl3u+2BI/kk+VEatbj0scLdrFhjPmbxOc1myhDP41ws= 51 | github.com/gomodule/redigo v1.8.9/go.mod h1:7ArFNvsTjH8GMMzB4uy1snslv2BwmginuMs06a1uzZE= 52 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 53 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 54 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 55 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 56 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 57 | github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= 58 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 59 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 60 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 61 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 62 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 63 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 64 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 65 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 66 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 67 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 68 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 69 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 70 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 71 | github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= 72 | github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= 73 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 74 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 75 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 76 | github.com/klauspost/compress v1.16.3 h1:XuJt9zzcnaz6a16/OU53ZjWp/v7/42WcR5t2a0PcNQY= 77 | github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 78 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 79 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= 80 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 81 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 82 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 83 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 84 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 85 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 86 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 87 | github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= 88 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 89 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 90 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 91 | github.com/mna/redisc v1.3.2 h1:sc9C+nj6qmrTFnsXb70xkjAHpXKtjjBuE6v2UcQV0ZE= 92 | github.com/mna/redisc v1.3.2/go.mod h1:CplIoaSTDi5h9icnj4FLbRgHoNKCHDNJDVRztWDGeSQ= 93 | github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= 94 | github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= 95 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 96 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 97 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 98 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 99 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 100 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 101 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 102 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 103 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 104 | github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= 105 | github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= 106 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 107 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 108 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 109 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 110 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 111 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 112 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 113 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 114 | github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 115 | github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 116 | github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 117 | github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 118 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 119 | github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 120 | github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 121 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 122 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 123 | github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= 124 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 125 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 126 | github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= 127 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 128 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 129 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 130 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 131 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 132 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 133 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 134 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 135 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 136 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 137 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 138 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 139 | github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 140 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 141 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 142 | github.com/valyala/fasthttp v1.48.0 h1:oJWvHb9BIZToTQS3MuQ2R3bJZiNSa2KiNdeI8A+79Tc= 143 | github.com/valyala/fasthttp v1.48.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= 144 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= 145 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 146 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 147 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 148 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 149 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 150 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 151 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= 152 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= 153 | github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= 154 | github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= 155 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= 156 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= 157 | github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= 158 | github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= 159 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 160 | go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ= 161 | go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= 162 | golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 163 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 164 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 165 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 166 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 167 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 168 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 169 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 170 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 171 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 172 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 173 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= 174 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 175 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 176 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 177 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 178 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 179 | golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 180 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 181 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 187 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 188 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 189 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 190 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 191 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 192 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 193 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 194 | golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= 195 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 196 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 197 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 198 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 199 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 200 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 201 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 202 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 203 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 204 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 205 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 206 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 207 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 208 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 209 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 210 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 211 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 212 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 213 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 214 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 215 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 216 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 217 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 218 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 219 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 220 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 221 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 222 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 223 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 224 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 225 | -------------------------------------------------------------------------------- /lifetime.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // LifeTime controls the session expiration datetime. 8 | type LifeTime struct { 9 | // Remember, tip for the future: 10 | // No need of gob.Register, because we embed the time.Time. 11 | // And serious bug which has a result of me spending my whole evening: 12 | // Because of gob encoding it doesn't encodes/decodes the other fields if time.Time is embedded 13 | // (this should be a bug(go1.9-rc1) or not. We don't care atm) 14 | time.Time 15 | timer *time.Timer 16 | } 17 | 18 | // Begin will begin the life based on the time.Now().Add(d). 19 | // Use `Continue` to continue from a stored time(database-based session does that). 20 | func (lt *LifeTime) Begin(d time.Duration, onExpire func()) { 21 | if d <= 0 { 22 | return 23 | } 24 | 25 | lt.Time = time.Now().Add(d) 26 | lt.timer = time.AfterFunc(d, onExpire) 27 | } 28 | 29 | // Revive will continue the life based on the stored Time. 30 | // Other words that could be used for this func are: Continue, Restore, Resc. 31 | func (lt *LifeTime) Revive(onExpire func()) { 32 | if lt.Time.IsZero() { 33 | return 34 | } 35 | 36 | now := time.Now() 37 | if lt.Time.After(now) { 38 | d := lt.Time.Sub(now) 39 | lt.timer = time.AfterFunc(d, onExpire) 40 | } 41 | } 42 | 43 | // Shift resets the lifetime based on "d". 44 | func (lt *LifeTime) Shift(d time.Duration) { 45 | if d > 0 && lt.timer != nil { 46 | lt.timer.Reset(d) 47 | } 48 | } 49 | 50 | // ExpireNow reduce the lifetime completely. 51 | func (lt *LifeTime) ExpireNow() { 52 | lt.Time = CookieExpireDelete 53 | if lt.timer != nil { 54 | lt.timer.Stop() 55 | } 56 | } 57 | 58 | // HasExpired reports whether "lt" represents is expired. 59 | func (lt *LifeTime) HasExpired() bool { 60 | if lt.IsZero() { 61 | return false 62 | } 63 | 64 | return lt.Time.Before(time.Now()) 65 | } 66 | 67 | // DurationUntilExpiration returns the duration until expires, it can return negative number if expired, 68 | // a call to `HasExpired` may be useful before calling this `Dur` function. 69 | func (lt *LifeTime) DurationUntilExpiration() time.Duration { 70 | return time.Until(lt.Time) 71 | } 72 | -------------------------------------------------------------------------------- /logo_900_273_bg_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kataras/go-sessions/2a50e79ae6a46b0cca3659682de8a582aa97dd71/logo_900_273_bg_white.png -------------------------------------------------------------------------------- /memstore.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func init() { 14 | gob.Register(Store{}) 15 | gob.Register(Entry{}) 16 | gob.Register(time.Time{}) 17 | } 18 | 19 | type ( 20 | // Entry is the entry of the context storage Store - .Values() 21 | Entry struct { 22 | Key string 23 | ValueRaw interface{} 24 | immutable bool // if true then it can't change by its caller. 25 | } 26 | 27 | // Store is a collection of key-value entries with immutability capabilities. 28 | // memstore contains a store which is just 29 | // a collection of key-value entries with immutability capabilities. 30 | // 31 | // Developers can use that storage to their own apps if they like its behavior. 32 | // It's fast and in the same time you get read-only access (safety) when you need it. 33 | Store []Entry 34 | ) 35 | 36 | // GetByKindOrNil will try to get this entry's value of "k" kind, 37 | // if value is not that kind it will NOT try to convert it the "k", instead 38 | // it will return nil, except if boolean; then it will return false 39 | // even if the value was not bool. 40 | // 41 | // If the "k" kind is not a string or int or int64 or bool 42 | // then it will return the raw value of the entry as it's. 43 | func (e Entry) GetByKindOrNil(k reflect.Kind) interface{} { 44 | switch k { 45 | case reflect.String: 46 | v := e.StringDefault("__$nf") 47 | if v == "__$nf" { 48 | return nil 49 | } 50 | return v 51 | case reflect.Int: 52 | v, err := e.IntDefault(-1) 53 | if err != nil || v == -1 { 54 | return nil 55 | } 56 | return v 57 | case reflect.Int64: 58 | v, err := e.Int64Default(-1) 59 | if err != nil || v == -1 { 60 | return nil 61 | } 62 | return v 63 | case reflect.Bool: 64 | v, err := e.BoolDefault(false) 65 | if err != nil { 66 | return nil 67 | } 68 | return v 69 | default: 70 | return e.ValueRaw 71 | } 72 | } 73 | 74 | // StringDefault returns the entry's value as string. 75 | // If not found returns "def". 76 | func (e Entry) StringDefault(def string) string { 77 | v := e.ValueRaw 78 | 79 | if vString, ok := v.(string); ok { 80 | return vString 81 | } 82 | 83 | return def 84 | } 85 | 86 | // String returns the entry's value as string. 87 | func (e Entry) String() string { 88 | return e.StringDefault("") 89 | } 90 | 91 | // StringTrim returns the entry's string value without trailing spaces. 92 | func (e Entry) StringTrim() string { 93 | return strings.TrimSpace(e.String()) 94 | } 95 | 96 | var errFindParse = "unable to find the %s with key: %s" 97 | 98 | // IntDefault returns the entry's value as int. 99 | // If not found returns "def" and a non-nil error. 100 | func (e Entry) IntDefault(def int) (int, error) { 101 | v := e.ValueRaw 102 | if v == nil { 103 | return def, fmt.Errorf(errFindParse, "int", e.Key) 104 | } 105 | if vint, ok := v.(int); ok { 106 | return vint, nil 107 | } else if vstring, sok := v.(string); sok && vstring != "" { 108 | vint, err := strconv.Atoi(vstring) 109 | if err != nil { 110 | return def, err 111 | } 112 | 113 | return vint, nil 114 | } 115 | 116 | return def, fmt.Errorf(errFindParse, "int", e.Key) 117 | } 118 | 119 | // Int64Default returns the entry's value as int64. 120 | // If not found returns "def" and a non-nil error. 121 | func (e Entry) Int64Default(def int64) (int64, error) { 122 | v := e.ValueRaw 123 | if v == nil { 124 | return def, fmt.Errorf(errFindParse, "int64", e.Key) 125 | } 126 | 127 | if vint64, ok := v.(int64); ok { 128 | return vint64, nil 129 | } 130 | 131 | if vint, ok := v.(int); ok { 132 | return int64(vint), nil 133 | } 134 | 135 | if vstring, sok := v.(string); sok { 136 | return strconv.ParseInt(vstring, 10, 64) 137 | } 138 | 139 | return def, fmt.Errorf(errFindParse, "int64", e.Key) 140 | } 141 | 142 | // Float64Default returns the entry's value as float64. 143 | // If not found returns "def" and a non-nil error. 144 | func (e Entry) Float64Default(def float64) (float64, error) { 145 | v := e.ValueRaw 146 | 147 | if v == nil { 148 | return def, fmt.Errorf(errFindParse, "float64", e.Key) 149 | } 150 | 151 | if vfloat32, ok := v.(float32); ok { 152 | return float64(vfloat32), nil 153 | } 154 | 155 | if vfloat64, ok := v.(float64); ok { 156 | return vfloat64, nil 157 | } 158 | 159 | if vint, ok := v.(int); ok { 160 | return float64(vint), nil 161 | } 162 | 163 | if vstring, sok := v.(string); sok { 164 | vfloat64, err := strconv.ParseFloat(vstring, 64) 165 | if err != nil { 166 | return def, err 167 | } 168 | 169 | return vfloat64, nil 170 | } 171 | 172 | return def, fmt.Errorf(errFindParse, "float64", e.Key) 173 | } 174 | 175 | // Float32Default returns the entry's value as float32. 176 | // If not found returns "def" and a non-nil error. 177 | func (e Entry) Float32Default(key string, def float32) (float32, error) { 178 | v := e.ValueRaw 179 | 180 | if v == nil { 181 | return def, fmt.Errorf(errFindParse, "float32", e.Key) 182 | } 183 | 184 | if vfloat32, ok := v.(float32); ok { 185 | return vfloat32, nil 186 | } 187 | 188 | if vfloat64, ok := v.(float64); ok { 189 | return float32(vfloat64), nil 190 | } 191 | 192 | if vint, ok := v.(int); ok { 193 | return float32(vint), nil 194 | } 195 | 196 | if vstring, sok := v.(string); sok { 197 | vfloat32, err := strconv.ParseFloat(vstring, 32) 198 | if err != nil { 199 | return def, err 200 | } 201 | 202 | return float32(vfloat32), nil 203 | } 204 | 205 | return def, fmt.Errorf(errFindParse, "float32", e.Key) 206 | } 207 | 208 | // BoolDefault returns the user's value as bool. 209 | // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" 210 | // or "0" or "f" or "F" or "FALSE" or "false" or "False". 211 | // Any other value returns an error. 212 | // 213 | // If not found returns "def" and a non-nil error. 214 | func (e Entry) BoolDefault(def bool) (bool, error) { 215 | v := e.ValueRaw 216 | if v == nil { 217 | return def, fmt.Errorf(errFindParse, "bool", e.Key) 218 | } 219 | 220 | if vBoolean, ok := v.(bool); ok { 221 | return vBoolean, nil 222 | } 223 | 224 | if vString, ok := v.(string); ok { 225 | b, err := strconv.ParseBool(vString) 226 | if err != nil { 227 | return def, err 228 | } 229 | return b, nil 230 | } 231 | 232 | if vInt, ok := v.(int); ok { 233 | if vInt == 1 { 234 | return true, nil 235 | } 236 | return false, nil 237 | } 238 | 239 | return def, fmt.Errorf(errFindParse, "bool", e.Key) 240 | } 241 | 242 | // Value returns the value of the entry, 243 | // respects the immutable. 244 | func (e Entry) Value() interface{} { 245 | if e.immutable { 246 | // take its value, no pointer even if setted with a reference. 247 | vv := reflect.Indirect(reflect.ValueOf(e.ValueRaw)) 248 | 249 | // return copy of that slice 250 | if vv.Type().Kind() == reflect.Slice { 251 | newSlice := reflect.MakeSlice(vv.Type(), vv.Len(), vv.Cap()) 252 | reflect.Copy(newSlice, vv) 253 | return newSlice.Interface() 254 | } 255 | // return a copy of that map 256 | if vv.Type().Kind() == reflect.Map { 257 | newMap := reflect.MakeMap(vv.Type()) 258 | for _, k := range vv.MapKeys() { 259 | newMap.SetMapIndex(k, vv.MapIndex(k)) 260 | } 261 | return newMap.Interface() 262 | } 263 | // if was *value it will return value{}. 264 | return vv.Interface() 265 | } 266 | return e.ValueRaw 267 | } 268 | 269 | // Save same as `Set` 270 | // However, if "immutable" is true then saves it as immutable (same as `SetImmutable`). 271 | // 272 | // 273 | // Returns the entry and true if it was just inserted, meaning that 274 | // it will return the entry and a false boolean if the entry exists and it has been updated. 275 | func (r *Store) Save(key string, value interface{}, immutable bool) (Entry, bool) { 276 | args := *r 277 | n := len(args) 278 | 279 | // replace if we can, else just return 280 | for i := 0; i < n; i++ { 281 | kv := &args[i] 282 | if kv.Key == key { 283 | if immutable && kv.immutable { 284 | // if called by `SetImmutable` 285 | // then allow the update, maybe it's a slice that user wants to update by SetImmutable method, 286 | // we should allow this 287 | kv.ValueRaw = value 288 | kv.immutable = immutable 289 | } else if kv.immutable == false { 290 | // if it was not immutable then user can alt it via `Set` and `SetImmutable` 291 | kv.ValueRaw = value 292 | kv.immutable = immutable 293 | } 294 | // else it was immutable and called by `Set` then disallow the update 295 | return *kv, false 296 | } 297 | } 298 | 299 | // expand slice to add it 300 | c := cap(args) 301 | if c > n { 302 | args = args[:n+1] 303 | kv := &args[n] 304 | kv.Key = key 305 | kv.ValueRaw = value 306 | kv.immutable = immutable 307 | *r = args 308 | return *kv, true 309 | } 310 | 311 | // add 312 | kv := Entry{ 313 | Key: key, 314 | ValueRaw: value, 315 | immutable: immutable, 316 | } 317 | *r = append(args, kv) 318 | return kv, true 319 | } 320 | 321 | // Set saves a value to the key-value storage. 322 | // Returns the entry and true if it was just inserted, meaning that 323 | // it will return the entry and a false boolean if the entry exists and it has been updated. 324 | // 325 | // See `SetImmutable` and `Get`. 326 | func (r *Store) Set(key string, value interface{}) (Entry, bool) { 327 | return r.Save(key, value, false) 328 | } 329 | 330 | // SetImmutable saves a value to the key-value storage. 331 | // Unlike `Set`, the output value cannot be changed by the caller later on (when .Get OR .Set) 332 | // 333 | // An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work 334 | // if the entry was immutable, for your own safety. 335 | // 336 | // Returns the entry and true if it was just inserted, meaning that 337 | // it will return the entry and a false boolean if the entry exists and it has been updated. 338 | // 339 | // Use it consistently, it's far slower than `Set`. 340 | // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 341 | func (r *Store) SetImmutable(key string, value interface{}) (Entry, bool) { 342 | return r.Save(key, value, true) 343 | } 344 | 345 | // GetEntry returns a pointer to the "Entry" found with the given "key" 346 | // if nothing found then it returns nil, so be careful with that, 347 | // it's not supposed to be used by end-developers. 348 | func (r *Store) GetEntry(key string) *Entry { 349 | args := *r 350 | n := len(args) 351 | for i := 0; i < n; i++ { 352 | kv := &args[i] 353 | if kv.Key == key { 354 | return kv 355 | } 356 | } 357 | 358 | return nil 359 | } 360 | 361 | // GetDefault returns the entry's value based on its key. 362 | // If not found returns "def". 363 | // This function checks for immutability as well, the rest don't. 364 | func (r *Store) GetDefault(key string, def interface{}) interface{} { 365 | v := r.GetEntry(key) 366 | if v == nil || v.ValueRaw == nil { 367 | return def 368 | } 369 | vv := v.Value() 370 | if vv == nil { 371 | return def 372 | } 373 | return vv 374 | } 375 | 376 | // Get returns the entry's value based on its key. 377 | // If not found returns nil. 378 | func (r *Store) Get(key string) interface{} { 379 | return r.GetDefault(key, nil) 380 | } 381 | 382 | // Visit accepts a visitor which will be filled 383 | // by the key-value objects. 384 | func (r *Store) Visit(visitor func(key string, value interface{})) { 385 | args := *r 386 | for i, n := 0, len(args); i < n; i++ { 387 | kv := args[i] 388 | visitor(kv.Key, kv.Value()) 389 | } 390 | } 391 | 392 | // GetStringDefault returns the entry's value as string, based on its key. 393 | // If not found returns "def". 394 | func (r *Store) GetStringDefault(key string, def string) string { 395 | v := r.GetEntry(key) 396 | if v == nil { 397 | return def 398 | } 399 | 400 | return v.StringDefault(def) 401 | } 402 | 403 | // GetString returns the entry's value as string, based on its key. 404 | func (r *Store) GetString(key string) string { 405 | return r.GetStringDefault(key, "") 406 | } 407 | 408 | // GetStringTrim returns the entry's string value without trailing spaces. 409 | func (r *Store) GetStringTrim(name string) string { 410 | return strings.TrimSpace(r.GetString(name)) 411 | } 412 | 413 | // GetInt returns the entry's value as int, based on its key. 414 | // If not found returns -1 and a non-nil error. 415 | func (r *Store) GetInt(key string) (int, error) { 416 | v := r.GetEntry(key) 417 | if v == nil { 418 | return 0, fmt.Errorf(errFindParse, "int", key) 419 | } 420 | return v.IntDefault(-1) 421 | } 422 | 423 | // GetIntDefault returns the entry's value as int, based on its key. 424 | // If not found returns "def". 425 | func (r *Store) GetIntDefault(key string, def int) int { 426 | if v, err := r.GetInt(key); err == nil { 427 | return v 428 | } 429 | 430 | return def 431 | } 432 | 433 | // GetInt64 returns the entry's value as int64, based on its key. 434 | // If not found returns -1 and a non-nil error. 435 | func (r *Store) GetInt64(key string) (int64, error) { 436 | v := r.GetEntry(key) 437 | if v == nil { 438 | return -1, fmt.Errorf(errFindParse, "int64", key) 439 | } 440 | return v.Int64Default(-1) 441 | } 442 | 443 | // GetInt64Default returns the entry's value as int64, based on its key. 444 | // If not found returns "def". 445 | func (r *Store) GetInt64Default(key string, def int64) int64 { 446 | if v, err := r.GetInt64(key); err == nil { 447 | return v 448 | } 449 | 450 | return def 451 | } 452 | 453 | // GetFloat64 returns the entry's value as float64, based on its key. 454 | // If not found returns -1 and a non nil error. 455 | func (r *Store) GetFloat64(key string) (float64, error) { 456 | v := r.GetEntry(key) 457 | if v == nil { 458 | return -1, fmt.Errorf(errFindParse, "float64", key) 459 | } 460 | return v.Float64Default(-1) 461 | } 462 | 463 | // GetFloat64Default returns the entry's value as float64, based on its key. 464 | // If not found returns "def". 465 | func (r *Store) GetFloat64Default(key string, def float64) float64 { 466 | if v, err := r.GetFloat64(key); err == nil { 467 | return v 468 | } 469 | 470 | return def 471 | } 472 | 473 | // GetBool returns the user's value as bool, based on its key. 474 | // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" 475 | // or "0" or "f" or "F" or "FALSE" or "false" or "False". 476 | // Any other value returns an error. 477 | // 478 | // If not found returns false and a non-nil error. 479 | func (r *Store) GetBool(key string) (bool, error) { 480 | v := r.GetEntry(key) 481 | if v == nil { 482 | return false, fmt.Errorf(errFindParse, "bool", key) 483 | } 484 | 485 | return v.BoolDefault(false) 486 | } 487 | 488 | // GetBoolDefault returns the user's value as bool, based on its key. 489 | // a string which is "1" or "t" or "T" or "TRUE" or "true" or "True" 490 | // or "0" or "f" or "F" or "FALSE" or "false" or "False". 491 | // 492 | // If not found returns "def". 493 | func (r *Store) GetBoolDefault(key string, def bool) bool { 494 | if v, err := r.GetBool(key); err == nil { 495 | return v 496 | } 497 | 498 | return def 499 | } 500 | 501 | // Remove deletes an entry linked to that "key", 502 | // returns true if an entry is actually removed. 503 | func (r *Store) Remove(key string) bool { 504 | args := *r 505 | n := len(args) 506 | for i := 0; i < n; i++ { 507 | kv := &args[i] 508 | if kv.Key == key { 509 | // we found the index, 510 | // let's remove the item by appending to the temp and 511 | // after set the pointer of the slice to this temp args 512 | args = append(args[:i], args[i+1:]...) 513 | *r = args 514 | return true 515 | } 516 | } 517 | return false 518 | } 519 | 520 | // Reset clears all the request entries. 521 | func (r *Store) Reset() { 522 | *r = (*r)[0:0] 523 | } 524 | 525 | // Len returns the full length of the entries. 526 | func (r *Store) Len() int { 527 | args := *r 528 | return len(args) 529 | } 530 | 531 | // Serialize returns the byte representation of the current Store. 532 | func (r Store) Serialize() []byte { 533 | w := new(bytes.Buffer) 534 | enc := gob.NewEncoder(w) 535 | err := enc.Encode(r) 536 | if err != nil { 537 | return nil 538 | } 539 | 540 | return w.Bytes() 541 | } 542 | -------------------------------------------------------------------------------- /provider.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type ( 10 | // provider contains the sessions and external databases (load and update). 11 | // It's the session memory manager 12 | provider struct { 13 | // we don't use RWMutex because all actions have read and write at the same action function. 14 | // (or write to a *Session's value which is race if we don't lock) 15 | // narrow locks are fasters but are useless here. 16 | mu sync.Mutex 17 | sessions map[string]*Session 18 | db Database 19 | destroyListeners []DestroyListener 20 | } 21 | ) 22 | 23 | // newProvider returns a new sessions provider 24 | func newProvider() *provider { 25 | return &provider{ 26 | sessions: make(map[string]*Session, 0), 27 | db: newMemDB(), 28 | } 29 | } 30 | 31 | // RegisterDatabase sets a session database. 32 | func (p *provider) RegisterDatabase(db Database) { 33 | p.mu.Lock() // for any case 34 | p.db = db 35 | p.mu.Unlock() 36 | } 37 | 38 | // newSession returns a new session from sessionid 39 | func (p *provider) newSession(sid string, expires time.Duration) *Session { 40 | onExpire := func() { 41 | p.Destroy(sid) 42 | } 43 | 44 | lifetime := p.db.Acquire(sid, expires) 45 | 46 | // simple and straight: 47 | if !lifetime.IsZero() { 48 | // if stored time is not zero 49 | // start a timer based on the stored time, if not expired. 50 | lifetime.Revive(onExpire) 51 | } else { 52 | // Remember: if db not exist or it has been expired 53 | // then the stored time will be zero(see loadSessionFromDB) and the values will be empty. 54 | // 55 | // Even if the database has an unlimited session (possible by a previous app run) 56 | // priority to the "expires" is given, 57 | // again if <=0 then it does nothing. 58 | lifetime.Begin(expires, onExpire) 59 | } 60 | 61 | sess := &Session{ 62 | sid: sid, 63 | provider: p, 64 | flashes: make(map[string]*flashMessage), 65 | Lifetime: lifetime, 66 | } 67 | 68 | return sess 69 | } 70 | 71 | // Init creates the session and returns it 72 | func (p *provider) Init(sid string, expires time.Duration) *Session { 73 | newSession := p.newSession(sid, expires) 74 | p.mu.Lock() 75 | p.sessions[sid] = newSession 76 | p.mu.Unlock() 77 | return newSession 78 | } 79 | 80 | // ErrNotFound can be returned when calling `UpdateExpiration` on a non-existing or invalid session entry. 81 | // It can be matched directly, i.e: `isErrNotFound := sessions.ErrNotFound.Equal(err)`. 82 | var ErrNotFound = errors.New("not found") 83 | 84 | // UpdateExpiration resets the expiration of a session. 85 | // if expires > 0 then it will try to update the expiration and destroy task is delayed. 86 | // if expires <= 0 then it does nothing it returns nil, to destroy a session call the `Destroy` func instead. 87 | // 88 | // If the session is not found, it returns a `NotFound` error, this can only happen when you restart the server and you used the memory-based storage(default), 89 | // because the call of the provider's `UpdateExpiration` is always called when the client has a valid session cookie. 90 | // 91 | // If a backend database is used then it may return an `ErrNotImplemented` error if the underline database does not support this operation. 92 | func (p *provider) UpdateExpiration(sid string, expires time.Duration) error { 93 | if expires <= 0 { 94 | return nil 95 | } 96 | 97 | p.mu.Lock() 98 | sess, found := p.sessions[sid] 99 | p.mu.Unlock() 100 | if !found { 101 | return ErrNotFound 102 | } 103 | 104 | sess.Lifetime.Shift(expires) 105 | return p.db.OnUpdateExpiration(sid, expires) 106 | } 107 | 108 | // Read returns the store which sid parameter belongs 109 | func (p *provider) Read(sid string, expires time.Duration) *Session { 110 | p.mu.Lock() 111 | if sess, found := p.sessions[sid]; found { 112 | sess.runFlashGC() // run the flash messages GC, new request here of existing session 113 | p.mu.Unlock() 114 | 115 | return sess 116 | } 117 | p.mu.Unlock() 118 | 119 | return p.Init(sid, expires) // if not found create new 120 | } 121 | 122 | func (p *provider) registerDestroyListener(ln DestroyListener) { 123 | if ln == nil { 124 | return 125 | } 126 | p.destroyListeners = append(p.destroyListeners, ln) 127 | } 128 | 129 | func (p *provider) fireDestroy(sid string) { 130 | for _, ln := range p.destroyListeners { 131 | ln(sid) 132 | } 133 | } 134 | 135 | // Destroy destroys the session, removes all sessions and flash values, 136 | // the session itself and updates the registered session databases, 137 | // this called from sessionManager which removes the client's cookie also. 138 | func (p *provider) Destroy(sid string) { 139 | p.mu.Lock() 140 | if sess, found := p.sessions[sid]; found { 141 | p.deleteSession(sess) 142 | } 143 | p.mu.Unlock() 144 | } 145 | 146 | // DestroyAll removes all sessions 147 | // from the server-side memory (and database if registered). 148 | // Client's session cookie will still exist but it will be reseted on the next request. 149 | func (p *provider) DestroyAll() { 150 | p.mu.Lock() 151 | for _, sess := range p.sessions { 152 | p.deleteSession(sess) 153 | } 154 | p.mu.Unlock() 155 | } 156 | 157 | func (p *provider) deleteSession(sess *Session) { 158 | sid := sess.sid 159 | 160 | delete(p.sessions, sid) 161 | p.db.Release(sid) 162 | p.fireDestroy(sid) 163 | } 164 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "sync" 7 | ) 8 | 9 | type ( 10 | // Session should expose the Sessions's end-user API. 11 | // It is the session's storage controller which you can 12 | // save or retrieve values based on a key. 13 | // 14 | // This is what will be returned when sess := sessions.Start(). 15 | Session struct { 16 | sid string 17 | isNew bool 18 | flashes map[string]*flashMessage 19 | mu sync.RWMutex // for flashes. 20 | Lifetime LifeTime 21 | provider *provider 22 | } 23 | 24 | flashMessage struct { 25 | // if true then this flash message is removed on the flash gc 26 | shouldRemove bool 27 | value interface{} 28 | } 29 | ) 30 | 31 | // Destroy destroys this session, it removes its session values and any flashes. 32 | // This session entry will be removed from the server, 33 | // the registered session databases will be notified for this deletion as well. 34 | // 35 | // Note that this method does NOT remove the client's cookie, although 36 | // it should be reseted if new session is attached to that (client). 37 | // 38 | // Use the session's manager `Destroy(ctx)` in order to remove the cookie as well. 39 | func (s *Session) Destroy() { 40 | s.provider.deleteSession(s) 41 | } 42 | 43 | // ID returns the session's ID. 44 | func (s *Session) ID() string { 45 | return s.sid 46 | } 47 | 48 | // IsNew returns true if this session is 49 | // created by the current application's process. 50 | func (s *Session) IsNew() bool { 51 | return s.isNew 52 | } 53 | 54 | // Get returns a value based on its "key". 55 | func (s *Session) Get(key string) interface{} { 56 | return s.provider.db.Get(s.sid, key) 57 | } 58 | 59 | // when running on the session manager removes any 'old' flash messages. 60 | func (s *Session) runFlashGC() { 61 | s.mu.Lock() 62 | for key, v := range s.flashes { 63 | if v.shouldRemove { 64 | delete(s.flashes, key) 65 | } 66 | } 67 | s.mu.Unlock() 68 | } 69 | 70 | // HasFlash returns true if this session has available flash messages. 71 | func (s *Session) HasFlash() bool { 72 | s.mu.RLock() 73 | has := len(s.flashes) > 0 74 | s.mu.RUnlock() 75 | return has 76 | } 77 | 78 | // GetFlash returns a stored flash message based on its "key" 79 | // which will be removed on the next request. 80 | // 81 | // To check for flash messages we use the HasFlash() Method 82 | // and to obtain the flash message we use the GetFlash() Method. 83 | // There is also a method GetFlashes() to fetch all the messages. 84 | // 85 | // Fetching a message deletes it from the session. 86 | // This means that a message is meant to be displayed only on the first page served to the user. 87 | func (s *Session) GetFlash(key string) interface{} { 88 | fv, ok := s.peekFlashMessage(key) 89 | if !ok { 90 | return nil 91 | } 92 | fv.shouldRemove = true 93 | return fv.value 94 | } 95 | 96 | // PeekFlash returns a stored flash message based on its "key". 97 | // Unlike GetFlash, this will keep the message valid for the next requests, 98 | // until GetFlashes or GetFlash("key"). 99 | func (s *Session) PeekFlash(key string) interface{} { 100 | fv, ok := s.peekFlashMessage(key) 101 | if !ok { 102 | return nil 103 | } 104 | return fv.value 105 | } 106 | 107 | func (s *Session) peekFlashMessage(key string) (*flashMessage, bool) { 108 | s.mu.RLock() 109 | fv, found := s.flashes[key] 110 | s.mu.RUnlock() 111 | 112 | if !found { 113 | return nil, false 114 | } 115 | 116 | return fv, true 117 | } 118 | 119 | // GetString same as Get but returns its string representation, 120 | // if key doesn't exist then it returns an empty string. 121 | func (s *Session) GetString(key string) string { 122 | if value := s.Get(key); value != nil { 123 | if v, ok := value.(string); ok { 124 | return v 125 | } 126 | 127 | if v, ok := value.(int); ok { 128 | return strconv.Itoa(v) 129 | } 130 | 131 | if v, ok := value.(int64); ok { 132 | return strconv.FormatInt(v, 10) 133 | } 134 | } 135 | 136 | return "" 137 | } 138 | 139 | // GetStringDefault same as Get but returns its string representation, 140 | // if key doesn't exist then it returns the "defaultValue". 141 | func (s *Session) GetStringDefault(key string, defaultValue string) string { 142 | if v := s.GetString(key); v != "" { 143 | return v 144 | } 145 | 146 | return defaultValue 147 | } 148 | 149 | // GetFlashString same as `GetFlash` but returns its string representation, 150 | // if key doesn't exist then it returns an empty string. 151 | func (s *Session) GetFlashString(key string) string { 152 | return s.GetFlashStringDefault(key, "") 153 | } 154 | 155 | // GetFlashStringDefault same as `GetFlash` but returns its string representation, 156 | // if key doesn't exist then it returns the "defaultValue". 157 | func (s *Session) GetFlashStringDefault(key string, defaultValue string) string { 158 | if value := s.GetFlash(key); value != nil { 159 | if v, ok := value.(string); ok { 160 | return v 161 | } 162 | } 163 | 164 | return defaultValue 165 | } 166 | 167 | // GetInt same as `Get` but returns its int representation, 168 | // if key doesn't exist then it returns -1 and a non-nil error. 169 | func (s *Session) GetInt(key string) (int, error) { 170 | v := s.Get(key) 171 | 172 | if vint, ok := v.(int); ok { 173 | return vint, nil 174 | } 175 | 176 | if vstring, sok := v.(string); sok { 177 | return strconv.Atoi(vstring) 178 | } 179 | 180 | return -1, fmt.Errorf(errFindParse, "int", key) 181 | } 182 | 183 | // GetIntDefault same as `Get` but returns its int representation, 184 | // if key doesn't exist then it returns the "defaultValue". 185 | func (s *Session) GetIntDefault(key string, defaultValue int) int { 186 | if v, err := s.GetInt(key); err == nil { 187 | return v 188 | } 189 | return defaultValue 190 | } 191 | 192 | // Increment increments the stored int value saved as "key" by +"n". 193 | // If value doesn't exist on that "key" then it creates one with the "n" as its value. 194 | // It returns the new, incremented, value. 195 | func (s *Session) Increment(key string, n int) (newValue int) { 196 | newValue = s.GetIntDefault(key, 0) 197 | newValue += n 198 | s.Set(key, newValue) 199 | return 200 | } 201 | 202 | // Decrement decrements the stored int value saved as "key" by -"n". 203 | // If value doesn't exist on that "key" then it creates one with the "n" as its value. 204 | // It returns the new, decremented, value even if it's less than zero. 205 | func (s *Session) Decrement(key string, n int) (newValue int) { 206 | newValue = s.GetIntDefault(key, 0) 207 | newValue -= n 208 | s.Set(key, newValue) 209 | return 210 | } 211 | 212 | // GetInt64 same as `Get` but returns its int64 representation, 213 | // if key doesn't exist then it returns -1 and a non-nil error. 214 | func (s *Session) GetInt64(key string) (int64, error) { 215 | v := s.Get(key) 216 | 217 | if vint64, ok := v.(int64); ok { 218 | return vint64, nil 219 | } 220 | 221 | if vint, ok := v.(int); ok { 222 | return int64(vint), nil 223 | } 224 | 225 | if vstring, sok := v.(string); sok { 226 | return strconv.ParseInt(vstring, 10, 64) 227 | } 228 | 229 | return -1, fmt.Errorf(errFindParse, "int64", key) 230 | } 231 | 232 | // GetInt64Default same as `Get` but returns its int64 representation, 233 | // if key doesn't exist it returns the "defaultValue". 234 | func (s *Session) GetInt64Default(key string, defaultValue int64) int64 { 235 | if v, err := s.GetInt64(key); err == nil { 236 | return v 237 | } 238 | 239 | return defaultValue 240 | } 241 | 242 | // GetFloat32 same as `Get` but returns its float32 representation, 243 | // if key doesn't exist then it returns -1 and a non-nil error. 244 | func (s *Session) GetFloat32(key string) (float32, error) { 245 | v := s.Get(key) 246 | 247 | if vfloat32, ok := v.(float32); ok { 248 | return vfloat32, nil 249 | } 250 | 251 | if vfloat64, ok := v.(float64); ok { 252 | return float32(vfloat64), nil 253 | } 254 | 255 | if vint, ok := v.(int); ok { 256 | return float32(vint), nil 257 | } 258 | 259 | if vstring, sok := v.(string); sok { 260 | vfloat64, err := strconv.ParseFloat(vstring, 32) 261 | if err != nil { 262 | return -1, err 263 | } 264 | return float32(vfloat64), nil 265 | } 266 | 267 | return -1, fmt.Errorf(errFindParse, "float32", key) 268 | } 269 | 270 | // GetFloat32Default same as `Get` but returns its float32 representation, 271 | // if key doesn't exist then it returns the "defaultValue". 272 | func (s *Session) GetFloat32Default(key string, defaultValue float32) float32 { 273 | if v, err := s.GetFloat32(key); err == nil { 274 | return v 275 | } 276 | 277 | return defaultValue 278 | } 279 | 280 | // GetFloat64 same as `Get` but returns its float64 representation, 281 | // if key doesn't exist then it returns -1 and a non-nil error. 282 | func (s *Session) GetFloat64(key string) (float64, error) { 283 | v := s.Get(key) 284 | 285 | if vfloat32, ok := v.(float32); ok { 286 | return float64(vfloat32), nil 287 | } 288 | 289 | if vfloat64, ok := v.(float64); ok { 290 | return vfloat64, nil 291 | } 292 | 293 | if vint, ok := v.(int); ok { 294 | return float64(vint), nil 295 | } 296 | 297 | if vstring, sok := v.(string); sok { 298 | return strconv.ParseFloat(vstring, 32) 299 | } 300 | 301 | return -1, fmt.Errorf(errFindParse, "float64", key) 302 | } 303 | 304 | // GetFloat64Default same as `Get` but returns its float64 representation, 305 | // if key doesn't exist then it returns the "defaultValue". 306 | func (s *Session) GetFloat64Default(key string, defaultValue float64) float64 { 307 | if v, err := s.GetFloat64(key); err == nil { 308 | return v 309 | } 310 | 311 | return defaultValue 312 | } 313 | 314 | // GetBoolean same as `Get` but returns its boolean representation, 315 | // if key doesn't exist then it returns false and a non-nil error. 316 | func (s *Session) GetBoolean(key string) (bool, error) { 317 | v := s.Get(key) 318 | if v == nil { 319 | return false, fmt.Errorf(errFindParse, "bool", key) 320 | } 321 | 322 | // here we could check for "true", "false" and 0 for false and 1 for true 323 | // but this may cause unexpected behavior from the developer if they expecting an error 324 | // so we just check if bool, if yes then return that bool, otherwise return false and an error. 325 | if vb, ok := v.(bool); ok { 326 | return vb, nil 327 | } 328 | if vstring, ok := v.(string); ok { 329 | return strconv.ParseBool(vstring) 330 | } 331 | 332 | return false, fmt.Errorf(errFindParse, "bool", key) 333 | } 334 | 335 | // GetBooleanDefault same as `Get` but returns its boolean representation, 336 | // if key doesn't exist then it returns the "defaultValue". 337 | func (s *Session) GetBooleanDefault(key string, defaultValue bool) bool { 338 | /* 339 | Note that here we can't do more than duplicate the GetBoolean's code, because of the "false". 340 | */ 341 | v := s.Get(key) 342 | if v == nil { 343 | return defaultValue 344 | } 345 | 346 | // here we could check for "true", "false" and 0 for false and 1 for true 347 | // but this may cause unexpected behavior from the developer if they expecting an error 348 | // so we just check if bool, if yes then return that bool, otherwise return false and an error. 349 | if vb, ok := v.(bool); ok { 350 | return vb 351 | } 352 | 353 | if vstring, ok := v.(string); ok { 354 | if b, err := strconv.ParseBool(vstring); err == nil { 355 | return b 356 | } 357 | } 358 | 359 | return defaultValue 360 | } 361 | 362 | // GetAll returns a copy of all session's values. 363 | func (s *Session) GetAll() map[string]interface{} { 364 | items := make(map[string]interface{}, s.provider.db.Len(s.sid)) 365 | s.mu.RLock() 366 | s.provider.db.Visit(s.sid, func(key string, value interface{}) { 367 | items[key] = value 368 | }) 369 | s.mu.RUnlock() 370 | return items 371 | } 372 | 373 | // GetFlashes returns all flash messages as map[string](key) and interface{} value 374 | // NOTE: this will cause at remove all current flash messages on the next request of the same user. 375 | func (s *Session) GetFlashes() map[string]interface{} { 376 | flashes := make(map[string]interface{}, len(s.flashes)) 377 | s.mu.Lock() 378 | for key, v := range s.flashes { 379 | flashes[key] = v.value 380 | v.shouldRemove = true 381 | } 382 | s.mu.Unlock() 383 | return flashes 384 | } 385 | 386 | // Visit loops each of the entries and calls the callback function func(key, value). 387 | func (s *Session) Visit(cb func(k string, v interface{})) { 388 | s.provider.db.Visit(s.sid, cb) 389 | } 390 | 391 | func (s *Session) set(key string, value interface{}, immutable bool) { 392 | s.provider.db.Set(s.sid, s.Lifetime, key, value, immutable) 393 | 394 | s.mu.Lock() 395 | s.isNew = false 396 | s.mu.Unlock() 397 | } 398 | 399 | // Set fills the session with an entry "value", based on its "key". 400 | func (s *Session) Set(key string, value interface{}) { 401 | s.set(key, value, false) 402 | } 403 | 404 | // SetImmutable fills the session with an entry "value", based on its "key". 405 | // Unlike `Set`, the output value cannot be changed by the caller later on (when .Get) 406 | // An Immutable entry should be only changed with a `SetImmutable`, simple `Set` will not work 407 | // if the entry was immutable, for your own safety. 408 | // Use it consistently, it's far slower than `Set`. 409 | // Read more about muttable and immutable go types: https://stackoverflow.com/a/8021081 410 | func (s *Session) SetImmutable(key string, value interface{}) { 411 | s.set(key, value, true) 412 | } 413 | 414 | // SetFlash sets a flash message by its key. 415 | // 416 | // A flash message is used in order to keep a message in session through one or several requests of the same user. 417 | // It is removed from session after it has been displayed to the user. 418 | // Flash messages are usually used in combination with HTTP redirections, 419 | // because in this case there is no view, so messages can only be displayed in the request that follows redirection. 420 | // 421 | // A flash message has a name and a content (AKA key and value). 422 | // It is an entry of an associative array. The name is a string: often "notice", "success", or "error", but it can be anything. 423 | // The content is usually a string. You can put HTML tags in your message if you display it raw. 424 | // You can also set the message value to a number or an array: it will be serialized and kept in session like a string. 425 | // 426 | // Flash messages can be set using the SetFlash() Method 427 | // For example, if you would like to inform the user that his changes were successfully saved, 428 | // you could add the following line to your Handler: 429 | // 430 | // SetFlash("success", "Data saved!"); 431 | // 432 | // In this example we used the key 'success'. 433 | // If you want to define more than one flash messages, you will have to use different keys. 434 | func (s *Session) SetFlash(key string, value interface{}) { 435 | s.mu.Lock() 436 | s.flashes[key] = &flashMessage{value: value} 437 | s.mu.Unlock() 438 | } 439 | 440 | // Delete removes an entry by its key, 441 | // returns true if actually something was removed. 442 | func (s *Session) Delete(key string) bool { 443 | removed := s.provider.db.Delete(s.sid, key) 444 | if removed { 445 | s.mu.Lock() 446 | s.isNew = false 447 | s.mu.Unlock() 448 | } 449 | 450 | return removed 451 | } 452 | 453 | // DeleteFlash removes a flash message by its key. 454 | func (s *Session) DeleteFlash(key string) { 455 | s.mu.Lock() 456 | delete(s.flashes, key) 457 | s.mu.Unlock() 458 | } 459 | 460 | // Clear removes all entries. 461 | func (s *Session) Clear() { 462 | s.mu.Lock() 463 | s.provider.db.Clear(s.sid) 464 | s.isNew = false 465 | s.mu.Unlock() 466 | } 467 | 468 | // ClearFlashes removes all flash messages. 469 | func (s *Session) ClearFlashes() { 470 | s.mu.Lock() 471 | for key := range s.flashes { 472 | delete(s.flashes, key) 473 | } 474 | s.mu.Unlock() 475 | } 476 | -------------------------------------------------------------------------------- /sessiondb/badger/database.go: -------------------------------------------------------------------------------- 1 | package badger 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "os" 8 | "runtime" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/kataras/go-sessions/v3" 13 | 14 | "github.com/dgraph-io/badger" 15 | ) 16 | 17 | // DefaultFileMode used as the default database's "fileMode" 18 | // for creating the sessions directory path, opening and write the session file. 19 | var ( 20 | DefaultFileMode = 0755 21 | ) 22 | 23 | // Database the badger(key-value file-based) session storage. 24 | type Database struct { 25 | // Service is the underline badger database connection, 26 | // it's initialized at `New` or `NewFromDB`. 27 | // Can be used to get stats. 28 | Service *badger.DB 29 | 30 | closed uint32 // if 1 is closed. 31 | } 32 | 33 | var _ sessions.Database = (*Database)(nil) 34 | 35 | // New creates and returns a new badger(key-value file-based) storage 36 | // instance based on the "directoryPath". 37 | // DirectoryPath should is the directory which the badger database will store the sessions, 38 | // i.e ./sessions 39 | // 40 | // It will remove any old session files. 41 | func New(directoryPath string) (*Database, error) { 42 | if directoryPath == "" { 43 | return nil, errors.New("directoryPath is missing") 44 | } 45 | 46 | lindex := directoryPath[len(directoryPath)-1] 47 | if lindex != os.PathSeparator && lindex != '/' { 48 | directoryPath += string(os.PathSeparator) 49 | } 50 | // create directories if necessary 51 | if err := os.MkdirAll(directoryPath, os.FileMode(DefaultFileMode)); err != nil { 52 | return nil, err 53 | } 54 | 55 | opts := badger.DefaultOptions(directoryPath) 56 | 57 | service, err := badger.Open(opts) 58 | 59 | if err != nil { 60 | log.Printf("unable to initialize the badger-based session database: %v\n", err) 61 | return nil, err 62 | } 63 | 64 | return NewFromDB(service), nil 65 | } 66 | 67 | // NewFromDB same as `New` but accepts an already-created custom badger connection instead. 68 | func NewFromDB(service *badger.DB) *Database { 69 | db := &Database{Service: service} 70 | 71 | runtime.SetFinalizer(db, closeDB) 72 | return db 73 | } 74 | 75 | // Acquire receives a session's lifetime from the database, 76 | // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. 77 | func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime { 78 | txn := db.Service.NewTransaction(true) 79 | defer txn.Commit() 80 | 81 | bsid := makePrefix(sid) 82 | item, err := txn.Get(bsid) 83 | if err == nil { 84 | // found, return the expiration. 85 | return sessions.LifeTime{Time: time.Unix(int64(item.ExpiresAt()), 0)} 86 | } 87 | 88 | // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. 89 | if err != nil { 90 | if err == badger.ErrKeyNotFound { 91 | // create it and set the expiration, we don't care about the value there. 92 | err = txn.SetEntry(badger.NewEntry(bsid, bsid).WithTTL(expires)) 93 | } 94 | } 95 | 96 | if err != nil { 97 | return sessions.LifeTime{Time: sessions.CookieExpireDelete} 98 | } 99 | 100 | return sessions.LifeTime{} // session manager will handle the rest. 101 | } 102 | 103 | // OnUpdateExpiration not implemented here, yet. 104 | // Note that this error will not be logged, callers should catch it manually. 105 | func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { 106 | return sessions.ErrNotImplemented 107 | } 108 | 109 | var delim = byte('_') 110 | 111 | func makePrefix(sid string) []byte { 112 | return append([]byte(sid), delim) 113 | } 114 | 115 | func makeKey(sid, key string) []byte { 116 | return append(makePrefix(sid), []byte(key)...) 117 | } 118 | 119 | // Set sets a key value of a specific session. 120 | // Ignore the "immutable". 121 | func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) { 122 | valueBytes, err := sessions.DefaultTranscoder.Marshal(value) 123 | if err != nil { 124 | return 125 | } 126 | 127 | db.Service.Update(func(txn *badger.Txn) error { 128 | dur := lifetime.DurationUntilExpiration() 129 | return txn.SetEntry(badger.NewEntry(makeKey(sid, key), valueBytes).WithTTL(dur)) 130 | }) 131 | } 132 | 133 | // Get retrieves a session value based on the key. 134 | func (db *Database) Get(sid string, key string) (value interface{}) { 135 | err := db.Service.View(func(txn *badger.Txn) error { 136 | item, err := txn.Get(makeKey(sid, key)) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | return item.Value(func(valueBytes []byte) error { 142 | return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) 143 | }) 144 | }) 145 | 146 | if err != nil && err != badger.ErrKeyNotFound { 147 | return nil 148 | } 149 | 150 | return 151 | } 152 | 153 | // validSessionItem reports whether the current iterator's item key 154 | // is a value of the session id "prefix". 155 | func validSessionItem(key, prefix []byte) bool { 156 | return len(key) > len(prefix) && bytes.Equal(key[0:len(prefix)], prefix) 157 | } 158 | 159 | // Visit loops through all session keys and values. 160 | func (db *Database) Visit(sid string, cb func(key string, value interface{})) { 161 | prefix := makePrefix(sid) 162 | 163 | txn := db.Service.NewTransaction(false) 164 | defer txn.Discard() 165 | 166 | iter := txn.NewIterator(badger.DefaultIteratorOptions) 167 | defer iter.Close() 168 | 169 | for iter.Rewind(); ; iter.Next() { 170 | if !iter.Valid() { 171 | break 172 | } 173 | 174 | item := iter.Item() 175 | key := item.Key() 176 | if !validSessionItem(key, prefix) { 177 | continue 178 | } 179 | 180 | var value interface{} 181 | 182 | err := item.Value(func(valueBytes []byte) error { 183 | return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) 184 | }) 185 | 186 | if err != nil { 187 | continue 188 | } 189 | 190 | cb(string(bytes.TrimPrefix(key, prefix)), value) 191 | } 192 | } 193 | 194 | var iterOptionsNoValues = badger.IteratorOptions{ 195 | PrefetchValues: false, 196 | PrefetchSize: 100, 197 | Reverse: false, 198 | AllVersions: false, 199 | } 200 | 201 | // Len returns the length of the session's entries (keys). 202 | func (db *Database) Len(sid string) (n int) { 203 | prefix := makePrefix(sid) 204 | 205 | txn := db.Service.NewTransaction(false) 206 | iter := txn.NewIterator(iterOptionsNoValues) 207 | 208 | for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() { 209 | n++ 210 | } 211 | 212 | iter.Close() 213 | txn.Discard() 214 | return 215 | } 216 | 217 | // Delete removes a session key value based on its key. 218 | func (db *Database) Delete(sid string, key string) (deleted bool) { 219 | txn := db.Service.NewTransaction(true) 220 | err := txn.Delete(makeKey(sid, key)) 221 | if err != nil { 222 | return 223 | } 224 | txn.Commit() 225 | return err == nil 226 | } 227 | 228 | // Clear removes all session key values but it keeps the session entry. 229 | func (db *Database) Clear(sid string) { 230 | prefix := makePrefix(sid) 231 | 232 | txn := db.Service.NewTransaction(true) 233 | defer txn.Commit() 234 | 235 | iter := txn.NewIterator(iterOptionsNoValues) 236 | defer iter.Close() 237 | 238 | for iter.Rewind(); iter.ValidForPrefix(prefix); iter.Next() { 239 | txn.Delete(iter.Item().Key()) 240 | } 241 | } 242 | 243 | // Release destroys the session, it clears and removes the session entry, 244 | // session manager will create a new session ID on the next request after this call. 245 | func (db *Database) Release(sid string) { 246 | // clear all $sid-$key. 247 | db.Clear(sid) 248 | // and remove the $sid. 249 | txn := db.Service.NewTransaction(true) 250 | txn.Delete([]byte(sid)) 251 | txn.Commit() 252 | } 253 | 254 | // Close shutdowns the badger connection. 255 | func (db *Database) Close() error { 256 | return closeDB(db) 257 | } 258 | 259 | func closeDB(db *Database) error { 260 | if atomic.LoadUint32(&db.closed) > 0 { 261 | return nil 262 | } 263 | err := db.Service.Close() 264 | if err == nil { 265 | atomic.StoreUint32(&db.closed, 1) 266 | } 267 | return err 268 | } 269 | -------------------------------------------------------------------------------- /sessiondb/boltdb/database.go: -------------------------------------------------------------------------------- 1 | package boltdb 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "time" 10 | 11 | "github.com/kataras/go-sessions/v3" 12 | 13 | bolt "go.etcd.io/bbolt" 14 | ) 15 | 16 | // DefaultFileMode used as the default database's "fileMode" 17 | // for creating the sessions directory path, opening and write 18 | // the session boltdb(file-based) storage. 19 | var ( 20 | DefaultFileMode = 0755 21 | ) 22 | 23 | // Database the BoltDB(file-based) session storage. 24 | type Database struct { 25 | table []byte 26 | // Service is the underline BoltDB database connection, 27 | // it's initialized at `New` or `NewFromDB`. 28 | // Can be used to get stats. 29 | Service *bolt.DB 30 | } 31 | 32 | var errPathMissing = errors.New("path is required") 33 | 34 | // New creates and returns a new BoltDB(file-based) storage 35 | // instance based on the "path". 36 | // Path should include the filename and the directory(aka fullpath), i.e sessions/store.db. 37 | // 38 | // It will remove any old session files. 39 | func New(path string, fileMode os.FileMode) (*Database, error) { 40 | if path == "" { 41 | return nil, errPathMissing 42 | } 43 | 44 | if fileMode <= 0 { 45 | fileMode = os.FileMode(DefaultFileMode) 46 | } 47 | 48 | // create directories if necessary 49 | if err := os.MkdirAll(filepath.Dir(path), fileMode); err != nil { 50 | return nil, err 51 | } 52 | 53 | service, err := bolt.Open(path, fileMode, 54 | &bolt.Options{Timeout: 20 * time.Second}, 55 | ) 56 | 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return NewFromDB(service, "sessions") 62 | } 63 | 64 | // NewFromDB same as `New` but accepts an already-created custom boltdb connection instead. 65 | func NewFromDB(service *bolt.DB, bucketName string) (*Database, error) { 66 | bucket := []byte(bucketName) 67 | 68 | service.Update(func(tx *bolt.Tx) (err error) { 69 | _, err = tx.CreateBucketIfNotExists(bucket) 70 | return 71 | }) 72 | 73 | db := &Database{table: bucket, Service: service} 74 | 75 | runtime.SetFinalizer(db, closeDB) 76 | return db, db.cleanup() 77 | } 78 | 79 | func (db *Database) getBucket(tx *bolt.Tx) *bolt.Bucket { 80 | return tx.Bucket(db.table) 81 | } 82 | 83 | func (db *Database) getBucketForSession(tx *bolt.Tx, sid string) *bolt.Bucket { 84 | b := db.getBucket(tx).Bucket([]byte(sid)) 85 | if b == nil { 86 | // session does not exist, it shouldn't happen, session bucket creation happens once at `Acquire`, 87 | // no need to accept the `bolt.bucket.CreateBucketIfNotExists`'s performance cost. 88 | return nil 89 | } 90 | 91 | return b 92 | } 93 | 94 | var ( 95 | expirationBucketName = []byte("expiration") 96 | delim = []byte("_") 97 | ) 98 | 99 | // expiration lives on its own bucket for each session bucket. 100 | func getExpirationBucketName(bsid []byte) []byte { 101 | return append(bsid, append(delim, expirationBucketName...)...) 102 | } 103 | 104 | // Cleanup removes any invalid(have expired) session entries on initialization. 105 | func (db *Database) cleanup() error { 106 | return db.Service.Update(func(tx *bolt.Tx) error { 107 | b := db.getBucket(tx) 108 | c := b.Cursor() 109 | // loop through all buckets, find one with expiration. 110 | for bsid, v := c.First(); bsid != nil; bsid, v = c.Next() { 111 | if len(bsid) == 0 { // empty key, continue to the next session bucket. 112 | continue 113 | } 114 | 115 | expirationName := getExpirationBucketName(bsid) 116 | if bExp := b.Bucket(expirationName); bExp != nil { // has expiration. 117 | _, expValue := bExp.Cursor().First() // the expiration bucket contains only one key(we don't care, see `Acquire`) value(time.Time) pair. 118 | if expValue == nil { 119 | log.Printf("cleanup: expiration is there but its value is empty '%s'\n", v) // should never happen. 120 | continue 121 | } 122 | 123 | var expirationTime time.Time 124 | if err := sessions.DefaultTranscoder.Unmarshal(expValue, &expirationTime); err != nil { 125 | log.Printf("cleanup: unable to retrieve expiration value for '%s'\n", v) 126 | continue 127 | } 128 | 129 | if expirationTime.Before(time.Now()) { 130 | // expired, delete the expiration bucket. 131 | if err := b.DeleteBucket(expirationName); err != nil { 132 | log.Printf("cleanup: unable to destroy a session '%s'\n", bsid) 133 | return err 134 | } 135 | 136 | // and the session bucket, if any. 137 | return b.DeleteBucket(bsid) 138 | } 139 | } 140 | } 141 | 142 | return nil 143 | }) 144 | } 145 | 146 | var expirationKey = []byte("exp") // it can be random. 147 | 148 | // Acquire receives a session's lifetime from the database, 149 | // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. 150 | func (db *Database) Acquire(sid string, expires time.Duration) (lifetime sessions.LifeTime) { 151 | bsid := []byte(sid) 152 | err := db.Service.Update(func(tx *bolt.Tx) (err error) { 153 | root := db.getBucket(tx) 154 | 155 | if expires > 0 { // should check or create the expiration bucket. 156 | name := getExpirationBucketName(bsid) 157 | b := root.Bucket(name) 158 | if b == nil { 159 | // not found, create a session bucket and an expiration bucket and save the given "expires" of time.Time, 160 | // don't return a lifetime, let it empty, session manager will do its job. 161 | b, err = root.CreateBucket(name) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | expirationTime := time.Now().Add(expires) 167 | timeBytes, err := sessions.DefaultTranscoder.Marshal(expirationTime) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | if err := b.Put(expirationKey, timeBytes); err == nil { 173 | // create the session bucket now, so the rest of the calls can be easly get the bucket without any further checks. 174 | _, err = root.CreateBucket(bsid) 175 | } 176 | 177 | return err 178 | } 179 | 180 | // found, get the associated expiration bucket, wrap its value and return. 181 | _, expValue := b.Cursor().First() 182 | if expValue == nil { 183 | return nil // does not expire. 184 | } 185 | 186 | var expirationTime time.Time 187 | if err = sessions.DefaultTranscoder.Unmarshal(expValue, &expirationTime); err != nil { 188 | return 189 | } 190 | 191 | lifetime = sessions.LifeTime{Time: expirationTime} 192 | return nil 193 | } 194 | 195 | // does not expire, just create the session bucket if not exists so we can be ready later on. 196 | _, err = root.CreateBucketIfNotExists(bsid) 197 | return 198 | }) 199 | 200 | if err != nil { 201 | return sessions.LifeTime{Time: sessions.CookieExpireDelete} 202 | } 203 | 204 | return 205 | } 206 | 207 | // OnUpdateExpiration will re-set the database's session's entry ttl. 208 | func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { 209 | expirationTime := time.Now().Add(newExpires) 210 | timeBytes, err := sessions.DefaultTranscoder.Marshal(expirationTime) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | return db.Service.Update(func(tx *bolt.Tx) error { 216 | expirationName := getExpirationBucketName([]byte(sid)) 217 | root := db.getBucket(tx) 218 | b := root.Bucket(expirationName) 219 | if b == nil { 220 | // golog.Debugf("tried to reset the expiration value for '%s' while its configured lifetime is unlimited or the session is already expired and not found now", sid) 221 | return sessions.ErrNotFound 222 | } 223 | 224 | return b.Put(expirationKey, timeBytes) 225 | }) 226 | } 227 | 228 | func makeKey(key string) []byte { 229 | return []byte(key) 230 | } 231 | 232 | // Set sets a key value of a specific session. 233 | // Ignore the "immutable". 234 | func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) { 235 | valueBytes, err := sessions.DefaultTranscoder.Marshal(value) 236 | if err != nil { 237 | return 238 | } 239 | 240 | db.Service.Update(func(tx *bolt.Tx) error { 241 | b := db.getBucketForSession(tx, sid) 242 | if b == nil { 243 | return nil 244 | } 245 | 246 | // Author's notes: 247 | // expiration is handlded by the session manager for the whole session, so the `db.Destroy` will be called when and if needed. 248 | // Therefore we don't have to implement a TTL here, but we need a `db.Cleanup`, as we did previously, method to delete any expired if server restarted 249 | // (badger does not need a `Cleanup` because we set the TTL based on the lifetime.DurationUntilExpiration()). 250 | return b.Put(makeKey(key), valueBytes) 251 | }) 252 | } 253 | 254 | // Get retrieves a session value based on the key. 255 | func (db *Database) Get(sid string, key string) (value interface{}) { 256 | db.Service.View(func(tx *bolt.Tx) error { 257 | b := db.getBucketForSession(tx, sid) 258 | if b == nil { 259 | return nil 260 | } 261 | 262 | valueBytes := b.Get(makeKey(key)) 263 | if len(valueBytes) == 0 { 264 | return nil 265 | } 266 | 267 | return sessions.DefaultTranscoder.Unmarshal(valueBytes, &value) 268 | }) 269 | 270 | return 271 | } 272 | 273 | // Visit loops through all session keys and values. 274 | func (db *Database) Visit(sid string, cb func(key string, value interface{})) { 275 | db.Service.View(func(tx *bolt.Tx) error { 276 | b := db.getBucketForSession(tx, sid) 277 | if b == nil { 278 | return nil 279 | } 280 | 281 | return b.ForEach(func(k []byte, v []byte) error { 282 | var value interface{} 283 | if err := sessions.DefaultTranscoder.Unmarshal(v, &value); err != nil { 284 | return err 285 | } 286 | 287 | cb(string(k), value) 288 | return nil 289 | }) 290 | }) 291 | } 292 | 293 | // Len returns the length of the session's entries (keys). 294 | func (db *Database) Len(sid string) (n int) { 295 | db.Service.View(func(tx *bolt.Tx) error { 296 | b := db.getBucketForSession(tx, sid) 297 | if b == nil { 298 | return nil 299 | } 300 | 301 | n = b.Stats().KeyN 302 | return nil 303 | }) 304 | 305 | return 306 | } 307 | 308 | // Delete removes a session key value based on its key. 309 | func (db *Database) Delete(sid string, key string) (deleted bool) { 310 | err := db.Service.Update(func(tx *bolt.Tx) error { 311 | b := db.getBucketForSession(tx, sid) 312 | if b == nil { 313 | return sessions.ErrNotFound 314 | } 315 | 316 | return b.Delete(makeKey(key)) 317 | }) 318 | 319 | return err == nil 320 | } 321 | 322 | // Clear removes all session key values but it keeps the session entry. 323 | func (db *Database) Clear(sid string) { 324 | db.Service.Update(func(tx *bolt.Tx) error { 325 | b := db.getBucketForSession(tx, sid) 326 | if b == nil { 327 | return nil 328 | } 329 | 330 | return b.ForEach(func(k []byte, v []byte) error { 331 | return b.Delete(k) 332 | }) 333 | }) 334 | } 335 | 336 | // Release destroys the session, it clears and removes the session entry, 337 | // session manager will create a new session ID on the next request after this call. 338 | func (db *Database) Release(sid string) { 339 | db.Service.Update(func(tx *bolt.Tx) error { 340 | // delete the session bucket. 341 | b := db.getBucket(tx) 342 | bsid := []byte(sid) 343 | // try to delete the associated expiration bucket, if exists, ignore error. 344 | b.DeleteBucket(getExpirationBucketName(bsid)) 345 | 346 | if err := b.DeleteBucket(bsid); err != nil { 347 | return err 348 | } 349 | 350 | return nil 351 | }) 352 | } 353 | 354 | // Close shutdowns the BoltDB connection. 355 | func (db *Database) Close() error { 356 | return closeDB(db) 357 | } 358 | 359 | func closeDB(db *Database) error { 360 | return db.Service.Close() 361 | } 362 | -------------------------------------------------------------------------------- /sessiondb/redis/database.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "log" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/kataras/go-sessions/v3" 9 | "github.com/kataras/go-sessions/v3/sessiondb/redis/service" 10 | ) 11 | 12 | // Database the redis back-end session database for the sessions. 13 | type Database struct { 14 | redis *service.Service 15 | } 16 | 17 | var _ sessions.Database = (*Database)(nil) 18 | 19 | // New returns a new redis database. 20 | func New(cfg ...service.Config) *Database { 21 | db := &Database{redis: service.New(cfg...)} 22 | db.redis.Connect() 23 | _, err := db.redis.PingPong() 24 | if err != nil { 25 | log.Fatal(err) 26 | return nil 27 | } 28 | runtime.SetFinalizer(db, closeDB) 29 | return db 30 | } 31 | 32 | // Config returns the configuration for the redis server bridge, you can change them. 33 | func (db *Database) Config() *service.Config { 34 | return db.redis.Config 35 | } 36 | 37 | // Acquire receives a session's lifetime from the database, 38 | // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. 39 | func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime { 40 | seconds, hasExpiration, found := db.redis.TTL(sid) 41 | if !found { 42 | // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. 43 | if err := db.redis.Set(sid, sid, int64(expires.Seconds())); err != nil { 44 | return sessions.LifeTime{Time: sessions.CookieExpireDelete} 45 | } 46 | 47 | return sessions.LifeTime{} // session manager will handle the rest. 48 | } 49 | 50 | if !hasExpiration { 51 | return sessions.LifeTime{} 52 | 53 | } 54 | 55 | return sessions.LifeTime{Time: time.Now().Add(time.Duration(seconds) * time.Second)} 56 | } 57 | 58 | // OnUpdateExpiration will re-set the database's session's entry ttl. 59 | // https://redis.io/commands/expire#refreshing-expires 60 | func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { 61 | return db.redis.UpdateTTLMany(sid, int64(newExpires.Seconds())) 62 | } 63 | 64 | const delim = "_" 65 | 66 | func makeKey(sid, key string) string { 67 | return sid + delim + key 68 | } 69 | 70 | // Set sets a key value of a specific session. 71 | // Ignore the "immutable". 72 | func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) { 73 | valueBytes, err := sessions.DefaultTranscoder.Marshal(value) 74 | if err != nil { 75 | return 76 | } 77 | 78 | db.redis.Set(makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())) 79 | } 80 | 81 | // Get retrieves a session value based on the key. 82 | func (db *Database) Get(sid string, key string) (value interface{}) { 83 | db.get(makeKey(sid, key), &value) 84 | return 85 | } 86 | 87 | func (db *Database) get(key string, outPtr interface{}) { 88 | data, err := db.redis.Get(key) 89 | if err != nil { 90 | // not found. 91 | return 92 | } 93 | 94 | sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr) 95 | } 96 | 97 | func (db *Database) keys(sid string) []string { 98 | keys, err := db.redis.GetKeys(sid + delim) 99 | if err != nil { 100 | return nil 101 | } 102 | 103 | return keys 104 | } 105 | 106 | // Visit loops through all session keys and values. 107 | func (db *Database) Visit(sid string, cb func(key string, value interface{})) { 108 | keys := db.keys(sid) 109 | for _, key := range keys { 110 | var value interface{} // new value each time, we don't know what user will do in "cb". 111 | db.get(key, &value) 112 | cb(key, value) 113 | } 114 | } 115 | 116 | // Len returns the length of the session's entries (keys). 117 | func (db *Database) Len(sid string) (n int) { 118 | return len(db.keys(sid)) 119 | } 120 | 121 | // Delete removes a session key value based on its key. 122 | func (db *Database) Delete(sid string, key string) (deleted bool) { 123 | return db.redis.Delete(makeKey(sid, key)) == nil 124 | } 125 | 126 | // Clear removes all session key values but it keeps the session entry. 127 | func (db *Database) Clear(sid string) { 128 | keys := db.keys(sid) 129 | for _, key := range keys { 130 | db.redis.Delete(key) 131 | } 132 | } 133 | 134 | // Release destroys the session, it clears and removes the session entry, 135 | // session manager will create a new session ID on the next request after this call. 136 | func (db *Database) Release(sid string) { 137 | // clear all $sid-$key. 138 | db.Clear(sid) 139 | // and remove the $sid. 140 | db.redis.Delete(sid) 141 | } 142 | 143 | // Close terminates the redis connection. 144 | func (db *Database) Close() error { 145 | return closeDB(db) 146 | } 147 | 148 | func closeDB(db *Database) error { 149 | return db.redis.CloseConnection() 150 | } 151 | -------------------------------------------------------------------------------- /sessiondb/redis/service/config.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | // DefaultRedisNetwork the redis network option, "tcp" 9 | DefaultRedisNetwork = "tcp" 10 | // DefaultRedisAddr the redis address option, "127.0.0.1:6379" 11 | DefaultRedisAddr = "127.0.0.1:6379" 12 | // DefaultRedisIdleTimeout the redis idle timeout option, time.Duration(5) * time.Minute 13 | DefaultRedisIdleTimeout = time.Duration(5) * time.Minute 14 | ) 15 | 16 | // Config the redis configuration used inside sessions 17 | type Config struct { 18 | // Network "tcp" 19 | Network string 20 | // Addr "127.0.0.1:6379" 21 | Addr string 22 | // Password string .If no password then no 'AUTH'. Default "" 23 | Password string 24 | // If Database is empty "" then no 'SELECT'. Default "" 25 | Database string 26 | // MaxIdle 0 no limit 27 | MaxIdle int 28 | // MaxActive 0 no limit 29 | MaxActive int 30 | // IdleTimeout time.Duration(5) * time.Minute 31 | IdleTimeout time.Duration 32 | // Prefix "myprefix-for-this-website". Default "" 33 | Prefix string 34 | } 35 | 36 | // DefaultConfig returns the default configuration for Redis service. 37 | func DefaultConfig() Config { 38 | return Config{ 39 | Network: DefaultRedisNetwork, 40 | Addr: DefaultRedisAddr, 41 | Password: "", 42 | Database: "", 43 | MaxIdle: 0, 44 | MaxActive: 0, 45 | IdleTimeout: DefaultRedisIdleTimeout, 46 | Prefix: "", 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sessiondb/redis/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/gomodule/redigo/redis" 9 | ) 10 | 11 | var ( 12 | // ErrRedisClosed an error with message 'Redis is already closed' 13 | ErrRedisClosed = errors.New("redis is already closed") 14 | // ErrKeyNotFound an error with message 'key not found' 15 | ErrKeyNotFound = errors.New("key not found") 16 | ) 17 | 18 | // Service the Redis service, contains the config and the redis pool 19 | type Service struct { 20 | // Connected is true when the Service has already connected 21 | Connected bool 22 | // Config the redis config for this redis 23 | Config *Config 24 | pool *redis.Pool 25 | } 26 | 27 | // PingPong sends a ping and receives a pong, if no pong received then returns false and filled error 28 | func (r *Service) PingPong() (bool, error) { 29 | c := r.pool.Get() 30 | defer c.Close() 31 | msg, err := c.Do("PING") 32 | if err != nil || msg == nil { 33 | return false, err 34 | } 35 | return (msg == "PONG"), nil 36 | } 37 | 38 | // CloseConnection closes the redis connection 39 | func (r *Service) CloseConnection() error { 40 | if r.pool != nil { 41 | return r.pool.Close() 42 | } 43 | return ErrRedisClosed 44 | } 45 | 46 | // Set sets a key-value to the redis store. 47 | // The expiration is setted by the MaxAgeSeconds. 48 | func (r *Service) Set(key string, value interface{}, secondsLifetime int64) (err error) { 49 | c := r.pool.Get() 50 | defer c.Close() 51 | if c.Err() != nil { 52 | return c.Err() 53 | } 54 | 55 | // if has expiration, then use the "EX" to delete the key automatically. 56 | if secondsLifetime > 0 { 57 | _, err = c.Do("SETEX", r.Config.Prefix+key, secondsLifetime, value) 58 | } else { 59 | _, err = c.Do("SET", r.Config.Prefix+key, value) 60 | } 61 | 62 | return 63 | } 64 | 65 | // Get returns value, err by its key 66 | // returns nil and a filled error if something bad happened. 67 | func (r *Service) Get(key string) (interface{}, error) { 68 | c := r.pool.Get() 69 | defer c.Close() 70 | if err := c.Err(); err != nil { 71 | return nil, err 72 | } 73 | 74 | redisVal, err := c.Do("GET", r.Config.Prefix+key) 75 | 76 | if err != nil { 77 | return nil, err 78 | } 79 | if redisVal == nil { 80 | return nil, ErrKeyNotFound 81 | } 82 | return redisVal, nil 83 | } 84 | 85 | // TTL returns the seconds to expire, if the key has expiration and error if action failed. 86 | // Read more at: https://redis.io/commands/ttl 87 | func (r *Service) TTL(key string) (seconds int64, hasExpiration bool, found bool) { 88 | c := r.pool.Get() 89 | defer c.Close() 90 | redisVal, err := c.Do("TTL", r.Config.Prefix+key) 91 | if err != nil { 92 | return -2, false, false 93 | } 94 | seconds = redisVal.(int64) 95 | // if -1 means the key has unlimited life time. 96 | hasExpiration = seconds > -1 97 | // if -2 means key does not exist. 98 | found = !(c.Err() != nil || seconds == -2) 99 | return 100 | } 101 | 102 | func (r *Service) updateTTLConn(c redis.Conn, key string, newSecondsLifeTime int64) error { 103 | reply, err := c.Do("EXPIRE", r.Config.Prefix+key, newSecondsLifeTime) 104 | 105 | if err != nil { 106 | return err 107 | } 108 | 109 | // https://redis.io/commands/expire#return-value 110 | // 111 | // 1 if the timeout was set. 112 | // 0 if key does not exist. 113 | if hadTTLOrExists, ok := reply.(int); ok { 114 | if hadTTLOrExists == 1 { 115 | return nil 116 | } else if hadTTLOrExists == 0 { 117 | return fmt.Errorf("unable to update expiration, the key '%s' was stored without ttl", key) 118 | } // do not check for -1. 119 | } 120 | 121 | return nil 122 | } 123 | 124 | // UpdateTTL will update the ttl of a key. 125 | // Using the "EXPIRE" command. 126 | // Read more at: https://redis.io/commands/expire#refreshing-expires 127 | func (r *Service) UpdateTTL(key string, newSecondsLifeTime int64) error { 128 | c := r.pool.Get() 129 | defer c.Close() 130 | err := c.Err() 131 | if err != nil { 132 | return err 133 | } 134 | 135 | return r.updateTTLConn(c, key, newSecondsLifeTime) 136 | } 137 | 138 | // UpdateTTLMany like `UpdateTTL` but for all keys starting with that "prefix", 139 | // it is a bit faster operation if you need to update all sessions keys (although it can be even faster if we used hash but this will limit other features), 140 | // look the `sessions/Database#OnUpdateExpiration` for example. 141 | func (r *Service) UpdateTTLMany(prefix string, newSecondsLifeTime int64) error { 142 | c := r.pool.Get() 143 | defer c.Close() 144 | if err := c.Err(); err != nil { 145 | return err 146 | } 147 | 148 | keys, err := r.getKeysConn(c, prefix) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | for _, key := range keys { 154 | if err = r.updateTTLConn(c, key, newSecondsLifeTime); err != nil { // fail on first error. 155 | return err 156 | } 157 | } 158 | 159 | return err 160 | } 161 | 162 | // GetAll returns all redis entries using the "SCAN" command (2.8+). 163 | func (r *Service) GetAll() (interface{}, error) { 164 | c := r.pool.Get() 165 | defer c.Close() 166 | if err := c.Err(); err != nil { 167 | return nil, err 168 | } 169 | 170 | redisVal, err := c.Do("SCAN", 0) // 0 -> cursor 171 | 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | if redisVal == nil { 177 | return nil, err 178 | } 179 | 180 | return redisVal, nil 181 | } 182 | 183 | func (r *Service) getKeysConn(c redis.Conn, prefix string) ([]string, error) { 184 | if err := c.Send("SCAN", 0, "MATCH", r.Config.Prefix+prefix+"*", "COUNT", 9999999999); err != nil { 185 | return nil, err 186 | } 187 | 188 | if err := c.Flush(); err != nil { 189 | return nil, err 190 | } 191 | 192 | reply, err := c.Receive() 193 | if err != nil || reply == nil { 194 | return nil, err 195 | } 196 | 197 | // it returns []interface, with two entries, the first one is "0" and the second one is a slice of the keys as []interface{uint8....}. 198 | 199 | if keysInterface, ok := reply.([]interface{}); ok { 200 | if len(keysInterface) == 2 { 201 | // take the second, it must contain the slice of keys. 202 | if keysSliceAsBytes, ok := keysInterface[1].([]interface{}); ok { 203 | keys := make([]string, len(keysSliceAsBytes), len(keysSliceAsBytes)) 204 | for i, k := range keysSliceAsBytes { 205 | keys[i] = fmt.Sprintf("%s", k)[len(r.Config.Prefix):] 206 | } 207 | 208 | return keys, nil 209 | } 210 | } 211 | } 212 | 213 | return nil, nil 214 | } 215 | 216 | // GetKeys returns all redis keys using the "SCAN" with MATCH command. 217 | // Read more at: https://redis.io/commands/scan#the-match-option. 218 | func (r *Service) GetKeys(prefix string) ([]string, error) { 219 | c := r.pool.Get() 220 | defer c.Close() 221 | if err := c.Err(); err != nil { 222 | return nil, err 223 | } 224 | 225 | return r.getKeysConn(c, prefix) 226 | } 227 | 228 | // GetBytes returns value, err by its key 229 | // you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{}) 230 | // returns nil and a filled error if something wrong happens 231 | func (r *Service) GetBytes(key string) ([]byte, error) { 232 | c := r.pool.Get() 233 | defer c.Close() 234 | if err := c.Err(); err != nil { 235 | return nil, err 236 | } 237 | 238 | redisVal, err := c.Do("GET", r.Config.Prefix+key) 239 | 240 | if err != nil { 241 | return nil, err 242 | } 243 | if redisVal == nil { 244 | return nil, ErrKeyNotFound 245 | } 246 | 247 | return redis.Bytes(redisVal, err) 248 | } 249 | 250 | // Delete removes redis entry by specific key 251 | func (r *Service) Delete(key string) error { 252 | c := r.pool.Get() 253 | defer c.Close() 254 | 255 | _, err := c.Do("DEL", r.Config.Prefix+key) 256 | return err 257 | } 258 | 259 | func dial(network string, addr string, pass string) (redis.Conn, error) { 260 | if network == "" { 261 | network = DefaultRedisNetwork 262 | } 263 | if addr == "" { 264 | addr = DefaultRedisAddr 265 | } 266 | c, err := redis.Dial(network, addr) 267 | if err != nil { 268 | return nil, err 269 | } 270 | if pass != "" { 271 | if _, err = c.Do("AUTH", pass); err != nil { 272 | c.Close() 273 | return nil, err 274 | } 275 | } 276 | return c, err 277 | } 278 | 279 | // Connect connects to the redis, called only once 280 | func (r *Service) Connect() { 281 | c := r.Config 282 | 283 | if c.IdleTimeout <= 0 { 284 | c.IdleTimeout = DefaultRedisIdleTimeout 285 | } 286 | 287 | if c.Network == "" { 288 | c.Network = DefaultRedisNetwork 289 | } 290 | 291 | if c.Addr == "" { 292 | c.Addr = DefaultRedisAddr 293 | } 294 | 295 | pool := &redis.Pool{IdleTimeout: DefaultRedisIdleTimeout, MaxIdle: c.MaxIdle, MaxActive: c.MaxActive} 296 | pool.TestOnBorrow = func(c redis.Conn, t time.Time) error { 297 | _, err := c.Do("PING") 298 | return err 299 | } 300 | 301 | if c.Database != "" { 302 | pool.Dial = func() (redis.Conn, error) { 303 | red, err := dial(c.Network, c.Addr, c.Password) 304 | if err != nil { 305 | return nil, err 306 | } 307 | if _, err = red.Do("SELECT", c.Database); err != nil { 308 | red.Close() 309 | return nil, err 310 | } 311 | return red, err 312 | } 313 | } else { 314 | pool.Dial = func() (redis.Conn, error) { 315 | return dial(c.Network, c.Addr, c.Password) 316 | } 317 | } 318 | r.Connected = true 319 | r.pool = pool 320 | } 321 | 322 | // New returns a Redis service filled by the passed config 323 | // to connect call the .Connect(). 324 | func New(cfg ...Config) *Service { 325 | c := DefaultConfig() 326 | if len(cfg) > 0 { 327 | c = cfg[0] 328 | } 329 | r := &Service{pool: &redis.Pool{}, Config: &c} 330 | return r 331 | } 332 | -------------------------------------------------------------------------------- /sessiondb/rediscluster/database.go: -------------------------------------------------------------------------------- 1 | package rediscluster 2 | 3 | import ( 4 | "log" 5 | "runtime" 6 | "time" 7 | 8 | "github.com/kataras/go-sessions/v3" 9 | "github.com/kataras/go-sessions/v3/sessiondb/rediscluster/service" 10 | ) 11 | 12 | // Database the redis back-end session database for the sessions. 13 | type Database struct { 14 | redis *service.Service 15 | } 16 | 17 | var _ sessions.Database = (*Database)(nil) 18 | 19 | // New returns a new redis database. 20 | func New(cfg ...service.Config) *Database { 21 | db := &Database{redis: service.New(cfg...)} 22 | db.redis.Connect() 23 | _, err := db.redis.PingPong() 24 | if err != nil { 25 | log.Fatal(err) 26 | return nil 27 | } 28 | runtime.SetFinalizer(db, closeDB) 29 | return db 30 | } 31 | 32 | // Config returns the configuration for the redis server bridge, you can change them. 33 | func (db *Database) Config() *service.Config { 34 | return db.redis.Config 35 | } 36 | 37 | // Acquire receives a session's lifetime from the database, 38 | // if the return value is LifeTime{} then the session manager sets the life time based on the expiration duration lives in configuration. 39 | func (db *Database) Acquire(sid string, expires time.Duration) sessions.LifeTime { 40 | seconds, hasExpiration, found := db.redis.TTL(sid) 41 | if !found { 42 | // not found, create an entry with ttl and return an empty lifetime, session manager will do its job. 43 | if err := db.redis.Set(sid, sid, int64(expires.Seconds())); err != nil { 44 | return sessions.LifeTime{Time: sessions.CookieExpireDelete} 45 | } 46 | 47 | return sessions.LifeTime{} // session manager will handle the rest. 48 | } 49 | 50 | if !hasExpiration { 51 | return sessions.LifeTime{} 52 | 53 | } 54 | 55 | return sessions.LifeTime{Time: time.Now().Add(time.Duration(seconds) * time.Second)} 56 | } 57 | 58 | // OnUpdateExpiration will re-set the database's session's entry ttl. 59 | // https://redis.io/commands/expire#refreshing-expires 60 | func (db *Database) OnUpdateExpiration(sid string, newExpires time.Duration) error { 61 | return db.redis.UpdateTTLMany(sid, int64(newExpires.Seconds())) 62 | } 63 | 64 | const delim = "_" 65 | 66 | func makeKey(sid, key string) string { 67 | return sid + delim + key 68 | } 69 | 70 | // Set sets a key value of a specific session. 71 | // Ignore the "immutable". 72 | func (db *Database) Set(sid string, lifetime sessions.LifeTime, key string, value interface{}, immutable bool) { 73 | valueBytes, err := sessions.DefaultTranscoder.Marshal(value) 74 | if err != nil { 75 | return 76 | } 77 | 78 | db.redis.Set(makeKey(sid, key), valueBytes, int64(lifetime.DurationUntilExpiration().Seconds())) 79 | } 80 | 81 | // Get retrieves a session value based on the key. 82 | func (db *Database) Get(sid string, key string) (value interface{}) { 83 | db.get(makeKey(sid, key), &value) 84 | return 85 | } 86 | 87 | func (db *Database) get(key string, outPtr interface{}) { 88 | data, err := db.redis.Get(key) 89 | if err != nil { 90 | // not found. 91 | return 92 | } 93 | 94 | sessions.DefaultTranscoder.Unmarshal(data.([]byte), outPtr) 95 | } 96 | 97 | func (db *Database) keys(sid string) []string { 98 | keys, err := db.redis.GetKeys(sid + delim) 99 | if err != nil { 100 | return nil 101 | } 102 | 103 | return keys 104 | } 105 | 106 | // Visit loops through all session keys and values. 107 | func (db *Database) Visit(sid string, cb func(key string, value interface{})) { 108 | keys := db.keys(sid) 109 | for _, key := range keys { 110 | var value interface{} // new value each time, we don't know what user will do in "cb". 111 | db.get(key, &value) 112 | cb(key, value) 113 | } 114 | } 115 | 116 | // Len returns the length of the session's entries (keys). 117 | func (db *Database) Len(sid string) (n int) { 118 | return len(db.keys(sid)) 119 | } 120 | 121 | // Delete removes a session key value based on its key. 122 | func (db *Database) Delete(sid string, key string) (deleted bool) { 123 | return db.redis.Delete(makeKey(sid, key)) == nil 124 | } 125 | 126 | // Clear removes all session key values but it keeps the session entry. 127 | func (db *Database) Clear(sid string) { 128 | keys := db.keys(sid) 129 | for _, key := range keys { 130 | db.redis.Delete(key) 131 | } 132 | } 133 | 134 | // Release destroys the session, it clears and removes the session entry, 135 | // session manager will create a new session ID on the next request after this call. 136 | func (db *Database) Release(sid string) { 137 | // clear all $sid-$key. 138 | db.Clear(sid) 139 | // and remove the $sid. 140 | db.redis.Delete(sid) 141 | } 142 | 143 | // Close terminates the redis connection. 144 | func (db *Database) Close() error { 145 | return closeDB(db) 146 | } 147 | 148 | func closeDB(db *Database) error { 149 | return db.redis.CloseConnection() 150 | } 151 | -------------------------------------------------------------------------------- /sessiondb/rediscluster/service/config.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | // DefaultRedisNetwork the redis network option, "tcp" 9 | DefaultRedisNetwork = "tcp" 10 | // DefaultRedisAddr the redis address option, "127.0.0.1:6379" 11 | DefaultRedisAddr = "127.0.0.1:6379" 12 | // DefaultRedisIdleTimeout the redis idle timeout option, time.Duration(5) * time.Minute 13 | DefaultRedisIdleTimeout = time.Duration(5) * time.Minute 14 | ) 15 | 16 | // Config the redis configuration used inside sessions 17 | type Config struct { 18 | // Network "tcp" 19 | Network string 20 | // Addr "127.0.0.1:6379" 21 | Addr string 22 | // Password string .If no password then no 'AUTH'. Default "" 23 | Password string 24 | // If Database is empty "" then no 'SELECT'. Default "" 25 | Database string 26 | // MaxIdle 0 no limit 27 | MaxIdle int 28 | // MaxActive 0 no limit 29 | MaxActive int 30 | // IdleTimeout time.Duration(5) * time.Minute 31 | IdleTimeout time.Duration 32 | // Prefix "myprefix-for-this-website". Default "" 33 | Prefix string 34 | } 35 | 36 | // DefaultConfig returns the default configuration for Redis service. 37 | func DefaultConfig() Config { 38 | return Config{ 39 | Network: DefaultRedisNetwork, 40 | Addr: DefaultRedisAddr, 41 | Password: "", 42 | Database: "", 43 | MaxIdle: 0, 44 | MaxActive: 0, 45 | IdleTimeout: DefaultRedisIdleTimeout, 46 | Prefix: "", 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sessiondb/rediscluster/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/gomodule/redigo/redis" 10 | "github.com/mna/redisc" 11 | ) 12 | 13 | var ( 14 | // ErrRedisClosed an error with message 'Redis is already closed' 15 | ErrRedisClosed = errors.New("redis is already closed") 16 | // ErrKeyNotFound an error with message 'key not found' 17 | ErrKeyNotFound = errors.New("key not found") 18 | ) 19 | 20 | // Service the Redis service, contains the config and the redis pool 21 | type Service struct { 22 | // Connected is true when the Service has already connected 23 | Connected bool 24 | // Config the redis config for this redis 25 | Config *Config 26 | pool *redisc.Cluster 27 | } 28 | 29 | // PingPong sends a ping and receives a pong, if no pong received then returns false and filled error 30 | func (r *Service) PingPong() (bool, error) { 31 | c := r.pool.Get() 32 | defer c.Close() 33 | msg, err := c.Do("PING") 34 | fmt.Println(msg, err) 35 | 36 | if err != nil || msg == nil { 37 | return false, err 38 | } 39 | return (msg == "PONG"), nil 40 | } 41 | 42 | // CloseConnection closes the redis connection 43 | func (r *Service) CloseConnection() error { 44 | if r.pool != nil { 45 | return r.pool.Close() 46 | } 47 | return ErrRedisClosed 48 | } 49 | 50 | // Set sets a key-value to the redis store. 51 | // The expiration is setted by the MaxAgeSeconds. 52 | func (r *Service) Set(key string, value interface{}, secondsLifetime int64) (err error) { 53 | c := r.pool.Get() 54 | defer c.Close() 55 | if c.Err() != nil { 56 | return c.Err() 57 | } 58 | 59 | // if has expiration, then use the "EX" to delete the key automatically. 60 | if secondsLifetime > 0 { 61 | _, err = c.Do("SETEX", r.Config.Prefix+key, secondsLifetime, value) 62 | } else { 63 | _, err = c.Do("SET", r.Config.Prefix+key, value) 64 | } 65 | 66 | return 67 | } 68 | 69 | // Get returns value, err by its key 70 | // returns nil and a filled error if something bad happened. 71 | func (r *Service) Get(key string) (interface{}, error) { 72 | c := r.pool.Get() 73 | defer c.Close() 74 | if err := c.Err(); err != nil { 75 | return nil, err 76 | } 77 | 78 | redisVal, err := c.Do("GET", r.Config.Prefix+key) 79 | 80 | if err != nil { 81 | return nil, err 82 | } 83 | if redisVal == nil { 84 | return nil, ErrKeyNotFound 85 | } 86 | return redisVal, nil 87 | } 88 | 89 | // TTL returns the seconds to expire, if the key has expiration and error if action failed. 90 | // Read more at: https://redis.io/commands/ttl 91 | func (r *Service) TTL(key string) (seconds int64, hasExpiration bool, found bool) { 92 | c := r.pool.Get() 93 | defer c.Close() 94 | redisVal, err := c.Do("TTL", r.Config.Prefix+key) 95 | if err != nil { 96 | return -2, false, false 97 | } 98 | seconds = redisVal.(int64) 99 | // if -1 means the key has unlimited life time. 100 | hasExpiration = seconds > -1 101 | // if -2 means key does not exist. 102 | found = !(c.Err() != nil || seconds == -2) 103 | return 104 | } 105 | 106 | func (r *Service) updateTTLConn(c redis.Conn, key string, newSecondsLifeTime int64) error { 107 | reply, err := c.Do("EXPIRE", r.Config.Prefix+key, newSecondsLifeTime) 108 | 109 | if err != nil { 110 | return err 111 | } 112 | 113 | // https://redis.io/commands/expire#return-value 114 | // 115 | // 1 if the timeout was set. 116 | // 0 if key does not exist. 117 | if hadTTLOrExists, ok := reply.(int); ok { 118 | if hadTTLOrExists == 1 { 119 | return nil 120 | } else if hadTTLOrExists == 0 { 121 | return fmt.Errorf("unable to update expiration, the key '%s' was stored without ttl", key) 122 | } // do not check for -1. 123 | } 124 | 125 | return nil 126 | } 127 | 128 | // UpdateTTL will update the ttl of a key. 129 | // Using the "EXPIRE" command. 130 | // Read more at: https://redis.io/commands/expire#refreshing-expires 131 | func (r *Service) UpdateTTL(key string, newSecondsLifeTime int64) error { 132 | c := r.pool.Get() 133 | defer c.Close() 134 | err := c.Err() 135 | if err != nil { 136 | return err 137 | } 138 | 139 | return r.updateTTLConn(c, key, newSecondsLifeTime) 140 | } 141 | 142 | // UpdateTTLMany like `UpdateTTL` but for all keys starting with that "prefix", 143 | // it is a bit faster operation if you need to update all sessions keys (although it can be even faster if we used hash but this will limit other features), 144 | // look the `sessions/Database#OnUpdateExpiration` for example. 145 | func (r *Service) UpdateTTLMany(prefix string, newSecondsLifeTime int64) error { 146 | c := r.pool.Get() 147 | defer c.Close() 148 | if err := c.Err(); err != nil { 149 | return err 150 | } 151 | 152 | keys, err := r.getKeysConn(c, prefix) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | for _, key := range keys { 158 | if err = r.updateTTLConn(c, key, newSecondsLifeTime); err != nil { // fail on first error. 159 | return err 160 | } 161 | } 162 | 163 | return err 164 | } 165 | 166 | // GetAll returns all redis entries using the "SCAN" command (2.8+). 167 | func (r *Service) GetAll() (interface{}, error) { 168 | c := r.pool.Get() 169 | defer c.Close() 170 | if err := c.Err(); err != nil { 171 | return nil, err 172 | } 173 | 174 | redisVal, err := c.Do("SCAN", 0) // 0 -> cursor 175 | 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | if redisVal == nil { 181 | return nil, err 182 | } 183 | 184 | return redisVal, nil 185 | } 186 | 187 | func (r *Service) getKeysConn(c redis.Conn, prefix string) ([]string, error) { 188 | if err := c.Send("SCAN", 0, "MATCH", r.Config.Prefix+prefix+"*", "COUNT", 9999999999); err != nil { 189 | return nil, err 190 | } 191 | 192 | if err := c.Flush(); err != nil { 193 | return nil, err 194 | } 195 | 196 | reply, err := c.Receive() 197 | if err != nil || reply == nil { 198 | return nil, err 199 | } 200 | 201 | // it returns []interface, with two entries, the first one is "0" and the second one is a slice of the keys as []interface{uint8....}. 202 | 203 | if keysInterface, ok := reply.([]interface{}); ok { 204 | if len(keysInterface) == 2 { 205 | // take the second, it must contain the slice of keys. 206 | if keysSliceAsBytes, ok := keysInterface[1].([]interface{}); ok { 207 | keys := make([]string, len(keysSliceAsBytes), len(keysSliceAsBytes)) 208 | for i, k := range keysSliceAsBytes { 209 | keys[i] = fmt.Sprintf("%s", k)[len(r.Config.Prefix):] 210 | } 211 | 212 | return keys, nil 213 | } 214 | } 215 | } 216 | 217 | return nil, nil 218 | } 219 | 220 | // GetKeys returns all redis keys using the "SCAN" with MATCH command. 221 | // Read more at: https://redis.io/commands/scan#the-match-option. 222 | func (r *Service) GetKeys(prefix string) ([]string, error) { 223 | c := r.pool.Get() 224 | defer c.Close() 225 | if err := c.Err(); err != nil { 226 | return nil, err 227 | } 228 | 229 | return r.getKeysConn(c, prefix) 230 | } 231 | 232 | // GetBytes returns value, err by its key 233 | // you can use utils.Deserialize((.GetBytes("yourkey"),&theobject{}) 234 | // returns nil and a filled error if something wrong happens 235 | func (r *Service) GetBytes(key string) ([]byte, error) { 236 | c := r.pool.Get() 237 | defer c.Close() 238 | if err := c.Err(); err != nil { 239 | return nil, err 240 | } 241 | 242 | redisVal, err := c.Do("GET", r.Config.Prefix+key) 243 | 244 | if err != nil { 245 | return nil, err 246 | } 247 | if redisVal == nil { 248 | return nil, ErrKeyNotFound 249 | } 250 | 251 | return redis.Bytes(redisVal, err) 252 | } 253 | 254 | // Delete removes redis entry by specific key 255 | func (r *Service) Delete(key string) error { 256 | c := r.pool.Get() 257 | defer c.Close() 258 | 259 | _, err := c.Do("DEL", r.Config.Prefix+key) 260 | return err 261 | } 262 | 263 | func dial(network string, addr string, pass string) (redis.Conn, error) { 264 | if network == "" { 265 | network = DefaultRedisNetwork 266 | } 267 | if addr == "" { 268 | addr = DefaultRedisAddr 269 | } 270 | c, err := redis.Dial(network, addr) 271 | if err != nil { 272 | return nil, err 273 | } 274 | if pass != "" { 275 | if _, err = c.Do("AUTH", pass); err != nil { 276 | c.Close() 277 | return nil, err 278 | } 279 | } 280 | return c, err 281 | } 282 | 283 | // Connect connects to the redis, called only once 284 | func (r *Service) Connect() { 285 | c := r.Config 286 | 287 | if c.IdleTimeout <= 0 { 288 | c.IdleTimeout = DefaultRedisIdleTimeout 289 | } 290 | 291 | if c.Network == "" { 292 | c.Network = DefaultRedisNetwork 293 | } 294 | 295 | if c.Addr == "" { 296 | c.Addr = DefaultRedisAddr 297 | } 298 | 299 | cluster := redisc.Cluster{ 300 | StartupNodes: []string{c.Addr}, 301 | DialOptions: []redis.DialOption{redis.DialConnectTimeout(5 * time.Second)}, 302 | CreatePool: func(address string, options ...redis.DialOption) (*redis.Pool, error) { 303 | return &redis.Pool{ 304 | MaxIdle: c.MaxIdle, 305 | MaxActive: c.MaxActive, 306 | IdleTimeout: c.IdleTimeout, 307 | Dial: func() (redis.Conn, error) { 308 | con, err := redis.Dial(c.Network, address, options...) 309 | if err != nil { 310 | return nil, err 311 | } 312 | 313 | if c.Password != "" { 314 | if _, err = con.Do("AUTH", c.Password); err != nil { 315 | con.Close() 316 | return nil, err 317 | } 318 | } 319 | 320 | if c.Database != "" { 321 | if _, err = con.Do("SELECT", c.Database); err != nil { 322 | con.Close() 323 | return nil, err 324 | } 325 | } 326 | 327 | return con, err 328 | }, 329 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 330 | _, err := c.Do("PING") 331 | return err 332 | }, 333 | }, nil 334 | }, 335 | } 336 | 337 | // initialize its mapping 338 | if err := cluster.Refresh(); err != nil { 339 | log.Fatalf("Refresh failed: %v", err) 340 | } 341 | 342 | r.pool = &cluster 343 | r.Connected = true 344 | } 345 | 346 | // New returns a Redis service filled by the passed config 347 | // to connect call the .Connect(). 348 | func New(cfg ...Config) *Service { 349 | c := DefaultConfig() 350 | if len(cfg) > 0 { 351 | c = cfg[0] 352 | } 353 | r := &Service{pool: &redisc.Cluster{}, Config: &c} 354 | return r 355 | } 356 | -------------------------------------------------------------------------------- /sessions.go: -------------------------------------------------------------------------------- 1 | // Package sessions provides sessions support for net/http and valyala/fasthttp 2 | // unique with auto-GC, register unlimited number of databases to Load and Update/Save the sessions in external server or to an external (no/or/and sql) database 3 | // Usage net/http: 4 | // // init a new sessions manager( if you use only one web framework inside your app then you can use the package-level functions like: sessions.Start/sessions.Destroy) 5 | // manager := sessions.New(sessions.Config{}) 6 | // // start a session for a particular client 7 | // manager.Start(http.ResponseWriter, *http.Request) 8 | // 9 | // // destroy a session from the server and client, 10 | // // don't call it on each handler, only on the handler you want the client to 'logout' or something like this: 11 | // manager.Destroy(http.ResponseWriter, *http.Request) 12 | // 13 | // 14 | // Usage valyala/fasthttp: 15 | // // init a new sessions manager( if you use only one web framework inside your app then you can use the package-level functions like: sessions.Start/sessions.Destroy) 16 | // manager := sessions.New(sessions.Config{}) 17 | // // start a session for a particular client 18 | // manager.StartFasthttp(*fasthttp.RequestCtx) 19 | // 20 | // // destroy a session from the server and client, 21 | // // don't call it on each handler, only on the handler you want the client to 'logout' or something like this: 22 | // manager.DestroyFasthttp(*fasthttp.Request) 23 | // 24 | // Note that, now, you can use both fasthttp and net/http within the same sessions manager(.New) instance! 25 | // So now, you can share sessions between a net/http app and valyala/fasthttp app 26 | package sessions 27 | 28 | import ( 29 | "net/http" 30 | "strings" 31 | "time" 32 | 33 | "github.com/valyala/fasthttp" 34 | ) 35 | 36 | const ( 37 | // Version current semantic version string of the go-sessions package. 38 | Version = "3.3.0" 39 | ) 40 | 41 | // A Sessions manager should be responsible to Start a sesion, based 42 | // on a Context, which should return 43 | // a compatible Session interface, type. If the external session manager 44 | // doesn't qualifies, then the user should code the rest of the functions with empty implementation. 45 | // 46 | // Sessions should be responsible to Destroy a session based 47 | // on the Context. 48 | type Sessions struct { 49 | config Config 50 | provider *provider 51 | } 52 | 53 | // Default instance of the sessions, used for package-level functions. 54 | var Default = New(Config{}.Validate()) 55 | 56 | // New returns the fast, feature-rich sessions manager. 57 | func New(cfg Config) *Sessions { 58 | return &Sessions{ 59 | config: cfg.Validate(), 60 | provider: newProvider(), 61 | } 62 | } 63 | 64 | // UseDatabase adds a session database to the manager's provider. 65 | func UseDatabase(db Database) { 66 | Default.UseDatabase(db) 67 | } 68 | 69 | // UseDatabase adds a session database to the manager's provider. 70 | func (s *Sessions) UseDatabase(db Database) { 71 | s.provider.RegisterDatabase(db) 72 | } 73 | 74 | // updateCookie gains the ability of updating the session browser cookie to any method which wants to update it 75 | func (s *Sessions) updateCookie(w http.ResponseWriter, r *http.Request, sid string, expires time.Duration) { 76 | cookie := &http.Cookie{} 77 | 78 | // The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding 79 | cookie.Name = s.config.Cookie 80 | 81 | cookie.Value = sid 82 | cookie.Path = "/" 83 | if !s.config.DisableSubdomainPersistence { 84 | 85 | requestDomain := r.URL.Host 86 | if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 { 87 | requestDomain = requestDomain[0:portIdx] 88 | } 89 | if IsValidCookieDomain(requestDomain) { 90 | 91 | // RFC2109, we allow level 1 subdomains, but no further 92 | // if we have localhost.com , we want the localhost.cos. 93 | // so if we have something like: mysubdomain.localhost.com we want the localhost here 94 | // if we have mysubsubdomain.mysubdomain.localhost.com we want the .mysubdomain.localhost.com here 95 | // slow things here, especially the 'replace' but this is a good and understable( I hope) way to get the be able to set cookies from subdomains & domain with 1-level limit 96 | if dotIdx := strings.LastIndexByte(requestDomain, '.'); dotIdx > 0 { 97 | // is mysubdomain.localhost.com || mysubsubdomain.mysubdomain.localhost.com 98 | s := requestDomain[0:dotIdx] // set mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost 99 | if secondDotIdx := strings.LastIndexByte(s, '.'); secondDotIdx > 0 { 100 | //is mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost 101 | s = s[secondDotIdx+1:] // set to localhost || mysubdomain.localhost 102 | } 103 | // replace the s with the requestDomain before the domain's siffux 104 | subdomainSuff := strings.LastIndexByte(requestDomain, '.') 105 | if subdomainSuff > len(s) { // if it is actual exists as subdomain suffix 106 | requestDomain = strings.Replace(requestDomain, requestDomain[0:subdomainSuff], s, 1) // set to localhost.com || mysubdomain.localhost.com 107 | } 108 | } 109 | // finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow) 110 | cookie.Domain = "." + requestDomain // . to allow persistence 111 | } 112 | } 113 | 114 | cookie.Domain = formatCookieDomain(r.URL.Host, s.config.DisableSubdomainPersistence) 115 | cookie.HttpOnly = true 116 | // MaxAge=0 means no 'Max-Age' attribute specified. 117 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 118 | // MaxAge>0 means Max-Age attribute present and given in seconds 119 | if expires >= 0 { 120 | if expires == 0 { // unlimited life 121 | cookie.Expires = CookieExpireUnlimited 122 | } else { // > 0 123 | cookie.Expires = time.Now().Add(expires) 124 | } 125 | cookie.MaxAge = int(cookie.Expires.Sub(time.Now()).Seconds()) 126 | } 127 | 128 | // set the cookie to secure if this is a tls wrapped request 129 | // and the configuration allows it. 130 | if r.TLS != nil && s.config.CookieSecureTLS { 131 | cookie.Secure = true 132 | } 133 | 134 | // encode the session id cookie client value right before send it. 135 | cookie.Value = s.encodeCookieValue(cookie.Value) 136 | AddCookie(w, r, cookie, s.config.AllowReclaim) 137 | } 138 | 139 | // Start starts the session for the particular request. 140 | func Start(w http.ResponseWriter, r *http.Request) *Session { 141 | return Default.Start(w, r) 142 | } 143 | 144 | // Start starts the session for the particular request. 145 | func (s *Sessions) Start(w http.ResponseWriter, r *http.Request) *Session { 146 | cookieValue := s.decodeCookieValue(GetCookie(r, s.config.Cookie)) 147 | 148 | if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie 149 | sid := s.config.SessionIDGenerator() 150 | 151 | sess := s.provider.Init(sid, s.config.Expires) 152 | sess.isNew = s.provider.db.Len(sid) == 0 153 | 154 | s.updateCookie(w, r, sid, s.config.Expires) 155 | 156 | return sess 157 | } 158 | 159 | sess := s.provider.Read(cookieValue, s.config.Expires) 160 | 161 | return sess 162 | } 163 | 164 | func (s *Sessions) updateCookieFasthttp(ctx *fasthttp.RequestCtx, sid string, expires time.Duration) { 165 | cookie := fasthttp.AcquireCookie() 166 | defer fasthttp.ReleaseCookie(cookie) 167 | 168 | // The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding 169 | cookie.SetKey(s.config.Cookie) 170 | 171 | cookie.SetValue(sid) 172 | cookie.SetPath("/") 173 | cookie.SetDomain(formatCookieDomain(string(ctx.Host()), s.config.DisableSubdomainPersistence)) 174 | 175 | cookie.SetHTTPOnly(true) 176 | // MaxAge=0 means no 'Max-Age' attribute specified. 177 | // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0' 178 | // MaxAge>0 means Max-Age attribute present and given in seconds 179 | if expires >= 0 { 180 | if expires == 0 { // unlimited life 181 | cookie.SetExpire(CookieExpireUnlimited) 182 | } else { // > 0 183 | cookie.SetExpire(time.Now().Add(expires)) 184 | } 185 | } 186 | 187 | // set the cookie to secure if this is a tls wrapped request 188 | // and the configuration allows it. 189 | 190 | if ctx.IsTLS() && s.config.CookieSecureTLS { 191 | cookie.SetSecure(true) 192 | } 193 | 194 | // encode the session id cookie client value right before send it. 195 | cookie.SetValue(s.encodeCookieValue(string(cookie.Value()))) 196 | AddCookieFasthttp(ctx, cookie) 197 | } 198 | 199 | // StartFasthttp starts the session for the particular request. 200 | func StartFasthttp(ctx *fasthttp.RequestCtx) *Session { 201 | return Default.StartFasthttp(ctx) 202 | } 203 | 204 | // StartFasthttp starts the session for the particular request. 205 | func (s *Sessions) StartFasthttp(ctx *fasthttp.RequestCtx) *Session { 206 | cookieValue := s.decodeCookieValue(GetCookieFasthttp(ctx, s.config.Cookie)) 207 | 208 | if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie 209 | sid := s.config.SessionIDGenerator() 210 | 211 | sess := s.provider.Init(sid, s.config.Expires) 212 | sess.isNew = s.provider.db.Len(sid) == 0 213 | 214 | s.updateCookieFasthttp(ctx, sid, s.config.Expires) 215 | 216 | return sess 217 | } 218 | 219 | sess := s.provider.Read(cookieValue, s.config.Expires) 220 | 221 | return sess 222 | } 223 | 224 | // ShiftExpiration move the expire date of a session to a new date 225 | // by using session default timeout configuration. 226 | func ShiftExpiration(w http.ResponseWriter, r *http.Request) { 227 | Default.ShiftExpiration(w, r) 228 | } 229 | 230 | // ShiftExpiration move the expire date of a session to a new date 231 | // by using session default timeout configuration. 232 | func (s *Sessions) ShiftExpiration(w http.ResponseWriter, r *http.Request) { 233 | s.UpdateExpiration(w, r, s.config.Expires) 234 | } 235 | 236 | // ShiftExpirationFasthttp move the expire date of a session to a new date 237 | // by using session default timeout configuration. 238 | func ShiftExpirationFasthttp(ctx *fasthttp.RequestCtx) { 239 | Default.ShiftExpirationFasthttp(ctx) 240 | } 241 | 242 | // ShiftExpirationFasthttp move the expire date of a session to a new date 243 | // by using session default timeout configuration. 244 | func (s *Sessions) ShiftExpirationFasthttp(ctx *fasthttp.RequestCtx) { 245 | s.UpdateExpirationFasthttp(ctx, s.config.Expires) 246 | } 247 | 248 | // UpdateExpiration change expire date of a session to a new date 249 | // by using timeout value passed by `expires` receiver. 250 | func UpdateExpiration(w http.ResponseWriter, r *http.Request, expires time.Duration) { 251 | Default.UpdateExpiration(w, r, expires) 252 | } 253 | 254 | // UpdateExpiration change expire date of a session to a new date 255 | // by using timeout value passed by `expires` receiver. 256 | // It will return `ErrNotFound` when trying to update expiration on a non-existence or not valid session entry. 257 | // It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet. 258 | func (s *Sessions) UpdateExpiration(w http.ResponseWriter, r *http.Request, expires time.Duration) error { 259 | cookieValue := s.decodeCookieValue(GetCookie(r, s.config.Cookie)) 260 | if cookieValue == "" { 261 | return ErrNotFound 262 | } 263 | 264 | // we should also allow it to expire when the browser closed 265 | err := s.provider.UpdateExpiration(cookieValue, expires) 266 | if err == nil || expires == -1 { 267 | s.updateCookie(w, r, cookieValue, expires) 268 | } 269 | 270 | return err 271 | } 272 | 273 | // UpdateExpirationFasthttp change expire date of a session to a new date 274 | // by using timeout value passed by `expires` receiver. 275 | func UpdateExpirationFasthttp(ctx *fasthttp.RequestCtx, expires time.Duration) { 276 | Default.UpdateExpirationFasthttp(ctx, expires) 277 | } 278 | 279 | // UpdateExpirationFasthttp change expire date of a session to a new date 280 | // by using timeout value passed by `expires` receiver. 281 | func (s *Sessions) UpdateExpirationFasthttp(ctx *fasthttp.RequestCtx, expires time.Duration) error { 282 | cookieValue := s.decodeCookieValue(GetCookieFasthttp(ctx, s.config.Cookie)) 283 | if cookieValue == "" { 284 | return ErrNotFound 285 | } 286 | 287 | // we should also allow it to expire when the browser closed 288 | err := s.provider.UpdateExpiration(cookieValue, expires) 289 | if err == nil || expires == -1 { 290 | s.updateCookieFasthttp(ctx, cookieValue, expires) 291 | } 292 | 293 | return err 294 | } 295 | 296 | func (s *Sessions) destroy(cookieValue string) { 297 | // decode the client's cookie value in order to find the server's session id 298 | // to destroy the session data. 299 | cookieValue = s.decodeCookieValue(cookieValue) 300 | if cookieValue == "" { // nothing to destroy 301 | return 302 | } 303 | 304 | s.provider.Destroy(cookieValue) 305 | } 306 | 307 | // DestroyListener is the form of a destroy listener. 308 | // Look `OnDestroy` for more. 309 | type DestroyListener func(sid string) 310 | 311 | // OnDestroy registers one or more destroy listeners. 312 | // A destroy listener is fired when a session has been removed entirely from the server (the entry) and client-side (the cookie). 313 | // Note that if a destroy listener is blocking, then the session manager will delay respectfully, 314 | // use a goroutine inside the listener to avoid that behavior. 315 | func (s *Sessions) OnDestroy(listeners ...DestroyListener) { 316 | for _, ln := range listeners { 317 | s.provider.registerDestroyListener(ln) 318 | } 319 | } 320 | 321 | // OnDestroy registers one or more destroy listeners. 322 | // A destroy listener is fired when a session has been removed entirely from the server (the entry) and client-side (the cookie). 323 | // Note that if a destroy listener is blocking, then the session manager will delay respectfully, 324 | // use a goroutine inside the listener to avoid that behavior. 325 | func OnDestroy(listeners ...DestroyListener) { 326 | Default.OnDestroy(listeners...) 327 | } 328 | 329 | // Destroy remove the session data and remove the associated cookie. 330 | func Destroy(w http.ResponseWriter, r *http.Request) { 331 | Default.Destroy(w, r) 332 | } 333 | 334 | // Destroy remove the session data and remove the associated cookie. 335 | func (s *Sessions) Destroy(w http.ResponseWriter, r *http.Request) { 336 | cookieValue := GetCookie(r, s.config.Cookie) 337 | s.destroy(cookieValue) 338 | RemoveCookie(w, r, s.config) 339 | } 340 | 341 | // DestroyFasthttp remove the session data and remove the associated cookie. 342 | func DestroyFasthttp(ctx *fasthttp.RequestCtx) { 343 | Default.DestroyFasthttp(ctx) 344 | } 345 | 346 | // DestroyFasthttp remove the session data and remove the associated cookie. 347 | func (s *Sessions) DestroyFasthttp(ctx *fasthttp.RequestCtx) { 348 | cookieValue := GetCookieFasthttp(ctx, s.config.Cookie) 349 | s.destroy(cookieValue) 350 | RemoveCookieFasthttp(ctx, s.config) 351 | } 352 | 353 | // DestroyByID removes the session entry 354 | // from the server-side memory (and database if registered). 355 | // Client's session cookie will still exist but it will be reseted on the next request. 356 | // 357 | // It's safe to use it even if you are not sure if a session with that id exists. 358 | // 359 | // Note: the sid should be the original one (i.e: fetched by a store ) 360 | // it's not decoded. 361 | func DestroyByID(sid string) { 362 | Default.DestroyByID(sid) 363 | } 364 | 365 | // DestroyByID removes the session entry 366 | // from the server-side memory (and database if registered). 367 | // Client's session cookie will still exist but it will be reseted on the next request. 368 | // 369 | // It's safe to use it even if you are not sure if a session with that id exists. 370 | // 371 | // Note: the sid should be the original one (i.e: fetched by a store ) 372 | // it's not decoded. 373 | func (s *Sessions) DestroyByID(sid string) { 374 | s.provider.Destroy(sid) 375 | } 376 | 377 | // DestroyAll removes all sessions 378 | // from the server-side memory (and database if registered). 379 | // Client's session cookie will still exist but it will be reseted on the next request. 380 | func DestroyAll() { 381 | Default.DestroyAll() 382 | } 383 | 384 | // DestroyAll removes all sessions 385 | // from the server-side memory (and database if registered). 386 | // Client's session cookie will still exist but it will be reseted on the next request. 387 | func (s *Sessions) DestroyAll() { 388 | s.provider.DestroyAll() 389 | } 390 | 391 | // let's keep these funcs simple, we can do it with two lines but we may add more things in the future. 392 | func (s *Sessions) decodeCookieValue(cookieValue string) string { 393 | if cookieValue == "" { 394 | return "" 395 | } 396 | 397 | var cookieValueDecoded *string 398 | 399 | if decode := s.config.Decode; decode != nil { 400 | err := decode(s.config.Cookie, cookieValue, &cookieValueDecoded) 401 | if err == nil { 402 | cookieValue = *cookieValueDecoded 403 | } else { 404 | cookieValue = "" 405 | } 406 | } 407 | 408 | return cookieValue 409 | } 410 | 411 | func (s *Sessions) encodeCookieValue(cookieValue string) string { 412 | if encode := s.config.Encode; encode != nil { 413 | newVal, err := encode(s.config.Cookie, cookieValue) 414 | if err == nil { 415 | cookieValue = newVal 416 | } else { 417 | cookieValue = "" 418 | } 419 | } 420 | 421 | return cookieValue 422 | } 423 | -------------------------------------------------------------------------------- /sessions_test.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "strings" 10 | "testing" 11 | 12 | // developers can use any library to add a custom cookie encoder/decoder. 13 | // At this test code we use the gorilla's securecookie library: 14 | "github.com/gavv/httpexpect" 15 | 16 | "github.com/gorilla/securecookie" 17 | ) 18 | 19 | var errReadBody = errors.New("While trying to read from the request body") 20 | 21 | // ReadJSON reads JSON from request's body 22 | func ReadJSON(jsonObject interface{}, req *http.Request) error { 23 | b, err := ioutil.ReadAll(req.Body) 24 | if err != nil && err != io.EOF { 25 | return err 26 | } 27 | decoder := json.NewDecoder(strings.NewReader(string(b))) 28 | err = decoder.Decode(jsonObject) 29 | 30 | if err != nil && err != io.EOF { 31 | return errReadBody 32 | } 33 | return nil 34 | } 35 | 36 | func getTester(mux *http.ServeMux, t *testing.T) *httpexpect.Expect { 37 | 38 | testConfiguration := httpexpect.Config{ 39 | BaseURL: "http://localhost:8080", 40 | Client: &http.Client{ 41 | Transport: httpexpect.NewBinder(mux), 42 | Jar: httpexpect.NewJar(), 43 | }, 44 | Reporter: httpexpect.NewAssertReporter(t), 45 | } 46 | 47 | return httpexpect.WithConfig(testConfiguration) 48 | } 49 | 50 | func writeValues(res http.ResponseWriter, values map[string]interface{}) error { 51 | result, err := json.Marshal(values) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | res.Header().Set("Content-Type", "application/json") 57 | res.Write(result) 58 | return nil 59 | } 60 | 61 | func TestSessionsNetHTTP(t *testing.T) { 62 | testSessionsNetHTTP(t) 63 | } 64 | 65 | func TestSessionsEncodeDecodeNetHTTP(t *testing.T) { 66 | 67 | // AES only supports key sizes of 16, 24 or 32 bytes. 68 | // You either need to provide exactly that amount or you derive the key from what you type in. 69 | hashKey := []byte("the-big-and-secret-fash-key-here") 70 | blockKey := []byte("lot-secret-of-characters-big-too") 71 | secureCookie := securecookie.New(hashKey, blockKey) 72 | // set the encode/decode funcs and run the test 73 | Default.config.Encode = secureCookie.Encode 74 | Default.config.Decode = secureCookie.Decode 75 | 76 | testSessionsNetHTTP(t) 77 | } 78 | 79 | func testSessionsNetHTTP(t *testing.T) { 80 | // enable parallel with net/http session values vs fasthttp session values when fasthttp tests too 81 | // t.Parallel() 82 | mux := http.NewServeMux() 83 | values := map[string]interface{}{ 84 | "Name": "go-sessions", 85 | "Days": "1", 86 | "Secret": "dsads£2132215£%%Ssdsa", 87 | } 88 | 89 | setHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 90 | vals := make(map[string]interface{}, 0) 91 | if err := ReadJSON(&vals, req); err != nil { 92 | t.Fatalf("Cannot readjson. Trace %s", err.Error()) 93 | } 94 | sess := Start(res, req) 95 | for k, v := range vals { 96 | sess.Set(k, v) 97 | } 98 | 99 | res.WriteHeader(http.StatusOK) 100 | }) 101 | mux.Handle("/set/", setHandler) 102 | 103 | writeSessValues := func(res http.ResponseWriter, req *http.Request) { 104 | sess := Start(res, req) 105 | sessValues := sess.GetAll() 106 | if err := writeValues(res, sessValues); err != nil { 107 | t.Fatalf("While serialize the session values: %s", err.Error()) 108 | } 109 | } 110 | 111 | getHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 112 | writeSessValues(res, req) 113 | }) 114 | mux.Handle("/get/", getHandler) 115 | 116 | clearHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 117 | sess := Start(res, req) 118 | sess.Clear() 119 | writeSessValues(res, req) 120 | }) 121 | mux.Handle("/clear/", clearHandler) 122 | 123 | destroyHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 124 | Destroy(res, req) 125 | writeSessValues(res, req) 126 | res.WriteHeader(http.StatusOK) 127 | // the cookie and all values should be empty 128 | }) 129 | mux.Handle("/destroy/", destroyHandler) 130 | 131 | destroyallHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 132 | DestroyAll() 133 | writeSessValues(res, req) 134 | res.WriteHeader(http.StatusOK) 135 | // the cookie and all values should be empty 136 | }) 137 | mux.Handle("/destroyall/", destroyallHandler) 138 | 139 | afterDestroyHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 140 | res.WriteHeader(http.StatusOK) 141 | }) 142 | // request cookie should be empty 143 | mux.Handle("/after_destroy/", afterDestroyHandler) 144 | 145 | e := getTester(mux, t) 146 | 147 | e.POST("/set/").WithJSON(values).Expect().Status(http.StatusOK).Cookies().NotEmpty() 148 | e.GET("/get/").Expect().Status(http.StatusOK).JSON().Object().Equal(values) 149 | 150 | // test destroy which also clears first 151 | d := e.GET("/destroy/").Expect().Status(http.StatusOK) 152 | d.JSON().Object().Empty() 153 | e.GET("/after_destroy/").Expect().Status(http.StatusOK).Cookies().Empty() 154 | // set and clear again 155 | e.POST("/set/").WithJSON(values).Expect().Status(http.StatusOK).Cookies().NotEmpty() 156 | e.GET("/clear/").Expect().Status(http.StatusOK).JSON().Object().Empty() 157 | // test destroy all (single) 158 | // destroy in order to set the full object without noise 159 | e.GET("/destroy/").Expect().Status(http.StatusOK).JSON().Object().Empty() 160 | // set,get and destroy with (all function) 161 | e.POST("/set/").WithJSON(values).Expect().Status(http.StatusOK) 162 | e.GET("/get/").Expect().Status(http.StatusOK).JSON().Object().Equal(values) 163 | // test destroy which also clears first 164 | e.GET("/destroyall/").Expect().Status(http.StatusOK).JSON().Object().Empty() 165 | e.GET("/after_destroy/").Expect().Status(http.StatusOK).Cookies().Empty() 166 | } 167 | 168 | func TestFlashMessages(t *testing.T) { 169 | // enable parallel with net/http session values vs fasthttp session values when fasthttp tests too 170 | // t.Parallel() 171 | mux := http.NewServeMux() 172 | 173 | valueSingleKey := "Name" 174 | valueSingleValue := "go-sessions" 175 | 176 | values := map[string]interface{}{ 177 | valueSingleKey: valueSingleValue, 178 | "Days": "1", 179 | "Secret": "dsads£2132215£%%Ssdsa", 180 | } 181 | 182 | setHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 183 | if req.RequestURI == "/set/" { 184 | vals := make(map[string]interface{}, 0) 185 | if err := ReadJSON(&vals, req); err != nil { 186 | t.Fatalf("Cannot readjson. Trace %s", err.Error()) 187 | } 188 | sess := Start(res, req) 189 | for k, v := range vals { 190 | sess.SetFlash(k, v) 191 | } 192 | 193 | res.WriteHeader(http.StatusOK) 194 | } 195 | }) 196 | mux.Handle("/set/", setHandler) 197 | 198 | writeFlashValues := func(res http.ResponseWriter, req *http.Request) { 199 | sess := Start(res, req) 200 | flashes := sess.GetFlashes() 201 | if err := writeValues(res, flashes); err != nil { 202 | t.Fatalf("While serialize the flash values: %s", err.Error()) 203 | } 204 | } 205 | 206 | getSingleHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 207 | if req.RequestURI == "/get_single/" { 208 | sess := Start(res, req) 209 | flashMsgString := sess.GetFlashString(valueSingleKey) 210 | res.Write([]byte(flashMsgString)) 211 | } 212 | }) 213 | 214 | mux.Handle("/get_single/", getSingleHandler) 215 | 216 | getHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 217 | if req.RequestURI == "/get/" { 218 | writeFlashValues(res, req) 219 | } 220 | }) 221 | 222 | mux.Handle("/get/", getHandler) 223 | 224 | clearHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 225 | if req.RequestURI == "/clear/" { 226 | sess := Start(res, req) 227 | sess.ClearFlashes() 228 | writeFlashValues(res, req) 229 | } 230 | }) 231 | 232 | mux.Handle("/clear/", clearHandler) 233 | 234 | destroyHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 235 | if req.RequestURI == "/destroy/" { 236 | Destroy(res, req) 237 | writeFlashValues(res, req) 238 | res.WriteHeader(http.StatusOK) 239 | } 240 | // the cookie and all values should be empty 241 | }) 242 | 243 | mux.Handle("/destroy/", destroyHandler) 244 | 245 | afterDestroyHandler := http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { 246 | if req.RequestURI == "/after_destroy/" { 247 | res.WriteHeader(http.StatusOK) 248 | } 249 | }) 250 | 251 | // request cookie should be empty 252 | mux.Handle("/after_destroy/", afterDestroyHandler) 253 | 254 | e := getTester(mux, t) 255 | 256 | e.POST("/set/").WithJSON(values).Expect().Status(http.StatusOK).Cookies().NotEmpty() 257 | // get all 258 | e.GET("/get/").Expect().Status(http.StatusOK).JSON().Object().Equal(values) 259 | // get the same flash on other request should return nothing because the flash message is removed after fetch once 260 | e.GET("/get/").Expect().Status(http.StatusOK).JSON().Object().Empty() 261 | // test destroy which also clears first 262 | d := e.GET("/destroy/").Expect().Status(http.StatusOK) 263 | d.JSON().Object().Empty() 264 | e.GET("/after_destroy/").Expect().Status(http.StatusOK).Cookies().Empty() 265 | // set and clear again 266 | e.POST("/set/").WithJSON(values).Expect().Status(http.StatusOK).Cookies().NotEmpty() 267 | e.GET("/clear/").Expect().Status(http.StatusOK).JSON().Object().Empty() 268 | 269 | // set again in order to take the single one ( we don't test Cookies.NotEmpty because httpexpect default conf reads that from the request-only) 270 | e.POST("/set/").WithJSON(values).Expect().Status(http.StatusOK) 271 | // e.GET("/get/").Expect().Status(http.StatusOK).JSON().Object().Equal(values) 272 | e.GET("/get_single/").Expect().Status(http.StatusOK).Body().Equal(valueSingleValue) 273 | } 274 | -------------------------------------------------------------------------------- /transcoding.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import "encoding/json" 4 | 5 | type ( 6 | // Marshaler is the common marshaler interface, used by transcoder. 7 | Marshaler interface { 8 | Marshal(interface{}) ([]byte, error) 9 | } 10 | // Unmarshaler is the common unmarshaler interface, used by transcoder. 11 | Unmarshaler interface { 12 | Unmarshal([]byte, interface{}) error 13 | } 14 | // Transcoder is the interface that transcoders should implement, it includes just the `Marshaler` and the `Unmarshaler`. 15 | Transcoder interface { 16 | Marshaler 17 | Unmarshaler 18 | } 19 | ) 20 | 21 | // DefaultTranscoder is the default transcoder across databases, it's the JSON by default. 22 | // Change it if you want a different serialization/deserialization inside your session databases (when `UseDatabase` is used). 23 | var DefaultTranscoder Transcoder = defaultTranscoder{} 24 | 25 | type defaultTranscoder struct{} 26 | 27 | func (d defaultTranscoder) Marshal(value interface{}) ([]byte, error) { 28 | if tr, ok := value.(Marshaler); ok { 29 | return tr.Marshal(value) 30 | } 31 | 32 | if jsonM, ok := value.(json.Marshaler); ok { 33 | return jsonM.MarshalJSON() 34 | } 35 | 36 | return json.Marshal(value) 37 | } 38 | 39 | func (d defaultTranscoder) Unmarshal(b []byte, outPtr interface{}) error { 40 | if tr, ok := outPtr.(Unmarshaler); ok { 41 | return tr.Unmarshal(b, outPtr) 42 | } 43 | 44 | if jsonUM, ok := outPtr.(json.Unmarshaler); ok { 45 | return jsonUM.UnmarshalJSON(b) 46 | } 47 | 48 | return json.Unmarshal(b, outPtr) 49 | } 50 | --------------------------------------------------------------------------------