diff --git a/.gitignore b/.gitignore index a1338d6..9e95391 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ + +.vscode diff --git a/.travis.yml b/.travis.yml index bc51dfc..b205250 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: go sudo: false go_import_path: github.com/go-session/session go: - - 1.7 + - 1.13 before_install: - go get -t -v ./... diff --git a/README.md b/README.md index 4f2284d..eb933b3 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # session -> A efficient, safely and easy-to-use session library for Go. +> A efficient, safely and easy-to-use session library for Go. -[![Build][Build-Status-Image]][Build-Status-Url] [![Codecov][codecov-image]][codecov-url] [![ReportCard][reportcard-image]][reportcard-url] [![GoDoc][godoc-image]][godoc-url] [![License][license-image]][license-url] +[![Build][build-status-image]][build-status-url] [![Codecov][codecov-image]][codecov-url] [![ReportCard][reportcard-image]][reportcard-url] [![GoDoc][godoc-image]][godoc-url] [![License][license-image]][license-url] ## Quick Start ### Download and install ```bash -$ go get -u -v github.com/go-session/session +go get -v github.com/go-session/session/v3 ``` ### Create file `server.go` @@ -22,7 +22,7 @@ import ( "fmt" "net/http" - "github.com/go-session/session" + session "github.com/go-session/session/v3" ) func main() { @@ -65,15 +65,17 @@ func main() { ### Build and run ```bash -$ go build server.go -$ ./server +go build server.go +./server ``` ### Open in your web browser +```text foo:bar +``` ## Features @@ -82,11 +84,13 @@ $ ./server - Multi-middleware support - More secure, signature-based tamper-proof - Context support +- Support request header and query parameters ## Store Implementations - [https://github.com/go-session/redis](https://github.com/go-session/redis) - Redis - [https://github.com/go-session/mongo](https://github.com/go-session/mongo) - MongoDB +- [https://github.com/go-session/gorm](https://github.com/go-session/gorm) - [GORM](https://github.com/jinzhu/gorm) - [https://github.com/go-session/mysql](https://github.com/go-session/mysql) - MySQL - [https://github.com/go-session/buntdb](https://github.com/go-session/buntdb) - [BuntDB](https://github.com/tidwall/buntdb) - [https://github.com/go-session/cookie](https://github.com/go-session/cookie) - Cookie @@ -100,10 +104,10 @@ $ ./server ## MIT License - Copyright (c) 2018 Lyric + Copyright (c) 2021 Lyric -[Build-Status-Url]: https://travis-ci.org/go-session/session -[Build-Status-Image]: https://travis-ci.org/go-session/session.svg?branch=master +[build-status-url]: https://travis-ci.org/go-session/session +[build-status-image]: https://travis-ci.org/go-session/session.svg?branch=master [codecov-url]: https://codecov.io/gh/go-session/session [codecov-image]: https://codecov.io/gh/go-session/session/branch/master/graph/badge.svg [reportcard-url]: https://goreportcard.com/report/github.com/go-session/session diff --git a/context.go b/context.go index 17baf60..9cde2a5 100644 --- a/context.go +++ b/context.go @@ -5,7 +5,7 @@ import ( "net/http" ) -// Define the key in the context +// Define the keys in the context type ( ctxResKey struct{} ctxReqKey struct{} diff --git a/context_test.go b/context_test.go index f769d81..2822166 100644 --- a/context_test.go +++ b/context_test.go @@ -3,7 +3,7 @@ package session import ( "context" "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "reflect" @@ -12,15 +12,15 @@ import ( func TestContext(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ctxKey interface{} = "ctx" - ctx := context.WithValue(context.Background(), ctxKey, "bar") + type ctxKey struct{} + ctx := context.WithValue(context.Background(), ctxKey{}, "bar") store, err := Start(ctx, w, r) if err != nil { t.Error(err) return } - ctxValue := store.Context().Value(ctxKey) + ctxValue := store.Context().Value(ctxKey{}) if !reflect.DeepEqual(ctxValue, "bar") { t.Error("Not expected value:", ctxValue) return @@ -48,7 +48,7 @@ func TestContext(t *testing.T) { return } - buf, _ := ioutil.ReadAll(res.Body) + buf, _ := io.ReadAll(res.Body) res.Body.Close() if string(buf) != "ok" { t.Error("Not expected value:", string(buf)) diff --git a/default.go b/default.go index dfd8979..7d8c1dc 100644 --- a/default.go +++ b/default.go @@ -18,7 +18,7 @@ func manager(opt ...Option) *Manager { return internalManager } -// InitManager initialize the global session management instance +// Initialize the global session management instance func InitManager(opt ...Option) { manager(opt...) } @@ -33,7 +33,7 @@ func Destroy(ctx context.Context, w http.ResponseWriter, r *http.Request) error return manager().Destroy(ctx, w, r) } -// Refresh a session and return to session storage +// Refresh and return session storage func Refresh(ctx context.Context, w http.ResponseWriter, r *http.Request) (Store, error) { return manager().Refresh(ctx, w, r) } diff --git a/default_test.go b/default_test.go index d452e1f..2b0ec36 100644 --- a/default_test.go +++ b/default_test.go @@ -2,7 +2,7 @@ package session import ( "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -20,7 +20,7 @@ func init() { func TestDefaultStart(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - store, err := Start(nil, w, r) + store, err := Start(r.Context(), w, r) if err != nil { t.Error(err) return @@ -59,7 +59,7 @@ func TestDefaultStart(t *testing.T) { So(err, ShouldBeNil) So(res, ShouldNotBeNil) - buf, err := ioutil.ReadAll(res.Body) + buf, err := io.ReadAll(res.Body) So(err, ShouldBeNil) res.Body.Close() So(string(buf), ShouldEqual, "bar:true") @@ -69,7 +69,7 @@ func TestDefaultStart(t *testing.T) { func TestDefaultDestroy(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("logout") == "1" { - err := Destroy(nil, w, r) + err := Destroy(r.Context(), w, r) if err != nil { t.Error(err) return @@ -78,7 +78,7 @@ func TestDefaultDestroy(t *testing.T) { return } - store, err := Start(nil, w, r) + store, err := Start(r.Context(), w, r) if err != nil { t.Error(err) return @@ -124,7 +124,7 @@ func TestDefaultDestroy(t *testing.T) { So(err, ShouldBeNil) So(res, ShouldNotBeNil) - buf, err := ioutil.ReadAll(res.Body) + buf, err := io.ReadAll(res.Body) So(err, ShouldBeNil) res.Body.Close() So(string(buf), ShouldEqual, ":false") @@ -133,14 +133,14 @@ func TestDefaultDestroy(t *testing.T) { func TestDefaultRefresh(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - store, err := Start(nil, w, r) + store, err := Start(r.Context(), w, r) if err != nil { t.Error(err) return } if r.URL.Query().Get("refresh") == "1" { - vstore, verr := Refresh(nil, w, r) + vstore, verr := Refresh(r.Context(), w, r) if verr != nil { t.Error(err) return @@ -185,7 +185,7 @@ func TestDefaultRefresh(t *testing.T) { So(len(res.Cookies()), ShouldBeGreaterThan, 0) So(res.Cookies()[0].Value, ShouldNotEqual, cookie.Value) - buf, err := ioutil.ReadAll(res.Body) + buf, err := io.ReadAll(res.Body) So(err, ShouldBeNil) res.Body.Close() So(string(buf), ShouldEqual, "bar:true") diff --git a/doc.go b/doc.go index 295a420..a6f3c1d 100644 --- a/doc.go +++ b/doc.go @@ -11,7 +11,7 @@ Example: "fmt" "net/http" - "github.com/go-session/session" + session "github.com/go-session/session/v3" ) func main() { diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dc71fae --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/go-session/session/v3 + +go 1.17 + +require ( + github.com/bytedance/gopkg v0.0.0-20221122125632-68358b8ecec6 + github.com/smartystreets/goconvey v1.6.4 +) + +require ( + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/jtolds/gls v4.20.0+incompatible // indirect + github.com/smartystreets/assertions v1.1.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..49172dc --- /dev/null +++ b/go.sum @@ -0,0 +1,25 @@ +github.com/bytedance/gopkg v0.0.0-20221122125632-68358b8ecec6 h1:FCLDGi1EmB7JzjVVYNZiqc/zAJj2BQ5M0lfkVOxbfs8= +github.com/bytedance/gopkg v0.0.0-20221122125632-68358b8ecec6/go.mod h1:5FoAH5xUHHCMDvQPy1rnj8moqLkLHFaDVBjHhcFwEi0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/session.go b/session.go index 7b11a34..1c3e11b 100644 --- a/session.go +++ b/session.go @@ -7,112 +7,166 @@ import ( "encoding/base64" "errors" "fmt" + "net" "net/http" "net/url" "strings" "time" ) +// Version # of session +const Version = "3.1.4" + var ( - // ErrInvalidSessionID invalid session id - ErrInvalidSessionID = errors.New("invalid session id") + ErrInvalidSessionID = errors.New("Invalid session id") ) +// Define the handler to get the session id +type IDHandlerFunc func(context.Context) string + // Define default options var defaultOptions = options{ cookieName: "go_session_id", cookieLifeTime: 3600 * 24 * 7, expired: 7200, secure: true, - sessionID: func() string { - return must(NewRandom()).String() + sameSite: http.SameSiteDefaultMode, + sessionID: func(_ context.Context) string { + return newUUID() }, + enableSetCookie: true, + enableSIDInURLQuery: true, } type options struct { - sign []byte - cookieName string - cookieLifeTime int - secure bool - domain string - expired int64 - sessionID func() string - store ManagerStore + sign []byte + cookieName string + cookieLifeTime int + secure bool + domain string + sameSite http.SameSite + expired int64 + sessionID IDHandlerFunc + enableSetCookie bool + enableSIDInURLQuery bool + enableSIDInHTTPHeader bool + sessionNameInHTTPHeader string + store ManagerStore } -// Option A session parameter options type Option func(*options) -// SetSign Set the session id signature value +// Set the session id signature value func SetSign(sign []byte) Option { return func(o *options) { o.sign = sign } } -// SetCookieName Set the cookie name +// Set the cookie name func SetCookieName(cookieName string) Option { return func(o *options) { o.cookieName = cookieName } } -// SetCookieLifeTime Set the cookie expiration time (in seconds) +// Set the cookie expiration time (in seconds) func SetCookieLifeTime(cookieLifeTime int) Option { return func(o *options) { o.cookieLifeTime = cookieLifeTime } } -// SetDomain Set the domain name of the cookie +// Set the domain name of the cookie func SetDomain(domain string) Option { return func(o *options) { o.domain = domain } } -// SetSecure Set cookie security +// Set cookie security func SetSecure(secure bool) Option { return func(o *options) { o.secure = secure } } -// SetExpired Set session expiration time (in seconds) +// Set SameSite attribute of the cookie +func SetSameSite(sameSite http.SameSite) Option { + return func(o *options) { + o.sameSite = sameSite + } +} + +// Set session expiration time (in seconds) func SetExpired(expired int64) Option { return func(o *options) { o.expired = expired } } -// SetSessionID Set callback function to generate session id -func SetSessionID(sessionID func() string) Option { +// Set callback function to generate session id +func SetSessionID(handler IDHandlerFunc) Option { + return func(o *options) { + o.sessionID = handler + } +} + +// Enable writing session id to cookie +// (enabled by default, can be turned off if no cookie is written) +func SetEnableSetCookie(enableSetCookie bool) Option { + return func(o *options) { + o.enableSetCookie = enableSetCookie + } +} + +// Allow session id from URL query parameters (enabled by default) +func SetEnableSIDInURLQuery(enableSIDInURLQuery bool) Option { + return func(o *options) { + o.enableSIDInURLQuery = enableSIDInURLQuery + } +} + +// Allow session id to be obtained from the request header +func SetEnableSIDInHTTPHeader(enableSIDInHTTPHeader bool) Option { return func(o *options) { - o.sessionID = sessionID + o.enableSIDInHTTPHeader = enableSIDInHTTPHeader } } -// SetStore Set session management storage +// The key name in the request header where the session ID is stored +// (if it is empty, the default is the cookie name) +func SetSessionNameInHTTPHeader(sessionNameInHTTPHeader string) Option { + return func(o *options) { + o.sessionNameInHTTPHeader = sessionNameInHTTPHeader + } +} + +// Set session management storage func SetStore(store ManagerStore) Option { return func(o *options) { o.store = store } } -// NewManager Create a session management instance +// Create a session management instance func NewManager(opt ...Option) *Manager { opts := defaultOptions for _, o := range opt { o(&opts) } + if opts.enableSIDInHTTPHeader && opts.sessionNameInHTTPHeader == "" { + opts.sessionNameInHTTPHeader = opts.cookieName + } + if opts.store == nil { opts.store = NewMemoryStore() } return &Manager{opts: &opts} } -// Manager A session management instance, including start and destroy operations +// A session management instance, including start and destroy operations type Manager struct { opts *options } @@ -157,10 +211,31 @@ func (m *Manager) decodeSessionID(value string) (string, error) { } func (m *Manager) sessionID(r *http.Request) (string, error) { - cookie, err := r.Cookie(m.opts.cookieName) - if err == nil && cookie.Value != "" { - return m.decodeSessionID(cookie.Value) + var cookieValue string + + if m.opts.enableSetCookie { + cookie, err := r.Cookie(m.opts.cookieName) + if err == nil && cookie.Value != "" { + cookieValue = cookie.Value + } + } + + if m.opts.enableSIDInURLQuery && cookieValue == "" { + err := r.ParseForm() + if err != nil { + return "", err + } + cookieValue = r.FormValue(m.opts.cookieName) + } + + if m.opts.enableSIDInHTTPHeader && cookieValue == "" { + cookieValue = r.Header.Get(m.opts.sessionNameInHTTPHeader) + } + + if cookieValue != "" { + return m.decodeSessionID(cookieValue) } + return "", nil } @@ -174,6 +249,11 @@ func (m *Manager) isSecure(r *http.Request) bool { if !m.opts.secure { return false } + host, _, _ := net.SplitHostPort(r.RemoteAddr) + ip := net.ParseIP(host) + if ip.IsLoopback() || ip.IsPrivate() { + return true + } if r.URL.Scheme != "" { return r.URL.Scheme == "https" } @@ -184,22 +264,33 @@ func (m *Manager) isSecure(r *http.Request) bool { } func (m *Manager) setCookie(sessionID string, w http.ResponseWriter, r *http.Request) { - cookie := &http.Cookie{ - Name: m.opts.cookieName, - Value: m.encodeSessionID(sessionID), - Path: "/", - HttpOnly: true, - Secure: m.isSecure(r), - Domain: m.opts.domain, - MaxAge: m.opts.cookieLifeTime, - } + cookieValue := m.encodeSessionID(sessionID) + + if m.opts.enableSetCookie { + cookie := &http.Cookie{ + Name: m.opts.cookieName, + Value: cookieValue, + Path: "/", + HttpOnly: true, + Secure: m.isSecure(r), + Domain: m.opts.domain, + SameSite: m.opts.sameSite, + } + + if v := m.opts.cookieLifeTime; v > 0 { + cookie.MaxAge = v + cookie.Expires = time.Now().Add(time.Duration(v) * time.Second) + } - if v := m.opts.cookieLifeTime; v > 0 { - cookie.Expires = time.Now().Add(time.Duration(v) * time.Second) + http.SetCookie(w, cookie) + r.AddCookie(cookie) } - http.SetCookie(w, cookie) - r.AddCookie(cookie) + if m.opts.enableSIDInHTTPHeader { + key := m.opts.sessionNameInHTTPHeader + r.Header.Set(key, cookieValue) + w.Header().Set(key, cookieValue) + } } // Start a session and return to session storage @@ -212,14 +303,15 @@ func (m *Manager) Start(ctx context.Context, w http.ResponseWriter, r *http.Requ } if sid != "" { - if exists, verr := m.opts.store.Check(ctx, sid); verr != nil { - return nil, verr + if exists, err := m.opts.store.Check(ctx, sid); err != nil { + return nil, err } else if exists { return m.opts.store.Update(ctx, sid, m.opts.expired) } } - store, err := m.opts.store.Create(ctx, m.opts.sessionID(), m.opts.expired) + sid = m.opts.sessionID(ctx) + store, err := m.opts.store.Create(ctx, sid, m.opts.expired) if err != nil { return nil, err } @@ -228,18 +320,19 @@ func (m *Manager) Start(ctx context.Context, w http.ResponseWriter, r *http.Requ return store, nil } -// Refresh a session and return to session storage +// Refresh and return session storage func (m *Manager) Refresh(ctx context.Context, w http.ResponseWriter, r *http.Request) (Store, error) { ctx = m.getContext(ctx, w, r) - sid, err := m.sessionID(r) + oldSID, err := m.sessionID(r) if err != nil { return nil, err - } else if sid == "" { - sid = m.opts.sessionID() + } else if oldSID == "" { + oldSID = m.opts.sessionID(ctx) } - store, err := m.opts.store.Refresh(ctx, sid, m.opts.sessionID(), m.opts.expired) + sid := m.opts.sessionID(ctx) + store, err := m.opts.store.Refresh(ctx, oldSID, sid, m.opts.expired) if err != nil { return nil, err } @@ -259,8 +352,7 @@ func (m *Manager) Destroy(ctx context.Context, w http.ResponseWriter, r *http.Re return nil } - exists, err := m.opts.store.Check(ctx, sid) - if err != nil { + if exists, err := m.opts.store.Check(ctx, sid); err != nil { return err } else if !exists { return nil @@ -271,14 +363,23 @@ func (m *Manager) Destroy(ctx context.Context, w http.ResponseWriter, r *http.Re return err } - cookie := &http.Cookie{ - Name: m.opts.cookieName, - Path: "/", - HttpOnly: true, - Expires: time.Now(), - MaxAge: -1, + if m.opts.enableSetCookie { + cookie := &http.Cookie{ + Name: m.opts.cookieName, + Path: "/", + HttpOnly: true, + Expires: time.Now(), + MaxAge: -1, + } + + http.SetCookie(w, cookie) + } + + if m.opts.enableSIDInHTTPHeader { + key := m.opts.sessionNameInHTTPHeader + r.Header.Del(key) + w.Header().Del(key) } - http.SetCookie(w, cookie) return nil } diff --git a/session_test.go b/session_test.go index 58f3669..c7ffc24 100644 --- a/session_test.go +++ b/session_test.go @@ -2,7 +2,7 @@ package session import ( "fmt" - "io/ioutil" + "io" "net/http" "net/http/httptest" "testing" @@ -17,7 +17,7 @@ func TestSessionStart(t *testing.T) { ) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - store, err := manager.Start(nil, w, r) + store, err := manager.Start(r.Context(), w, r) if err != nil { t.Error(err) return @@ -56,7 +56,7 @@ func TestSessionStart(t *testing.T) { So(err, ShouldBeNil) So(res, ShouldNotBeNil) - buf, err := ioutil.ReadAll(res.Body) + buf, err := io.ReadAll(res.Body) So(err, ShouldBeNil) res.Body.Close() So(string(buf), ShouldEqual, "bar:true") @@ -72,7 +72,7 @@ func TestSessionDestroy(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Query().Get("logout") == "1" { - err := manager.Destroy(nil, w, r) + err := manager.Destroy(r.Context(), w, r) if err != nil { t.Error(err) return @@ -81,7 +81,7 @@ func TestSessionDestroy(t *testing.T) { return } - store, err := manager.Start(nil, w, r) + store, err := manager.Start(r.Context(), w, r) if err != nil { t.Error(err) return @@ -127,7 +127,7 @@ func TestSessionDestroy(t *testing.T) { So(err, ShouldBeNil) So(res, ShouldNotBeNil) - buf, err := ioutil.ReadAll(res.Body) + buf, err := io.ReadAll(res.Body) So(err, ShouldBeNil) res.Body.Close() So(string(buf), ShouldEqual, ":false") @@ -142,14 +142,14 @@ func TestSessionRefresh(t *testing.T) { ) ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - store, err := manager.Start(nil, w, r) + store, err := manager.Start(r.Context(), w, r) if err != nil { t.Error(err) return } if r.URL.Query().Get("refresh") == "1" { - vstore, verr := manager.Refresh(nil, w, r) + vstore, verr := manager.Refresh(r.Context(), w, r) if verr != nil { t.Error(err) return @@ -194,7 +194,7 @@ func TestSessionRefresh(t *testing.T) { So(len(res.Cookies()), ShouldBeGreaterThan, 0) So(res.Cookies()[0].Value, ShouldNotEqual, cookie.Value) - buf, err := ioutil.ReadAll(res.Body) + buf, err := io.ReadAll(res.Body) So(err, ShouldBeNil) res.Body.Close() So(string(buf), ShouldEqual, "bar:true") diff --git a/store.go b/store.go index d9a3660..da60210 100644 --- a/store.go +++ b/store.go @@ -1,10 +1,11 @@ package session import ( - "container/list" "context" "sync" "time" + + "github.com/bytedance/gopkg/collection/skipmap" ) var ( @@ -13,7 +14,7 @@ var ( now = time.Now ) -// ManagerStore Management of session storage, including creation, update, and delete operations +// Management of session storage, including creation, update, and delete operations type ManagerStore interface { // Check the session store exists Check(ctx context.Context, sid string) (bool, error) @@ -29,7 +30,7 @@ type ManagerStore interface { Close() error } -// Store A session id storage operation +// A session id storage operation type Store interface { // Get a session storage context Context() context.Context @@ -47,22 +48,14 @@ type Store interface { Flush() error } -// NewMemoryStore create an instance of a memory store +// Create a new session storage (memory) func NewMemoryStore() ManagerStore { mstore := &memoryStore{ - data: make(map[string]*dataItem), - list: list.New(), ticker: time.NewTicker(time.Second), - } - - mstore.pool = sync.Pool{ - New: func() interface{} { - return newStore(mstore) - }, + data: skipmap.NewString(), } go mstore.gc() - return mstore } @@ -81,124 +74,78 @@ func newDataItem(sid string, values map[string]interface{}, expired int64) *data } type memoryStore struct { - sync.RWMutex - pool sync.Pool - data map[string]*dataItem - list *list.List ticker *time.Ticker + data *skipmap.StringMap } func (s *memoryStore) gc() { for range s.ticker.C { - s.RLock() - e := s.list.Front() - s.RUnlock() - - for e != nil { - item := e.Value.(*dataItem) - if item.expiredAt.Before(now()) { - s.Lock() - s.list.Remove(e) - delete(s.data, item.sid) - e = e.Next() - s.Unlock() - } else { - break + s.data.Range(func(key string, value interface{}) bool { + if item, ok := value.(*dataItem); ok && item.expiredAt.Before(now()) { + s.data.Delete(key) } - } + return true + }) } } func (s *memoryStore) save(sid string, values map[string]interface{}, expired int64) { - s.Lock() - defer s.Unlock() - - if item, ok := s.data[sid]; ok { - item.values = values + if dt, ok := s.data.Load(sid); ok { + dt.(*dataItem).values = values return } - item := newDataItem(sid, values, expired) - s.data[sid] = item - s.list.PushBack(item) + s.data.Store(sid, newDataItem(sid, values, expired)) } -func (s *memoryStore) Check(_ context.Context, sid string) (bool, error) { - s.RLock() - item, ok := s.data[sid] - s.RUnlock() +func (s *memoryStore) Check(ctx context.Context, sid string) (bool, error) { + dt, ok := s.data.Load(sid) + if !ok { + return false, nil + } - if ok && item.expiredAt.After(now()) { + if item, ok := dt.(*dataItem); ok && item.expiredAt.After(now()) { return true, nil } return false, nil } func (s *memoryStore) Create(ctx context.Context, sid string, expired int64) (Store, error) { - store := s.pool.Get().(*store) - store.reset(ctx, sid, expired, nil) - return store, nil + return newStore(ctx, s, sid, expired, nil), nil } func (s *memoryStore) Update(ctx context.Context, sid string, expired int64) (Store, error) { - store := s.pool.Get().(*store) - s.Lock() - defer s.Unlock() - - item, ok := s.data[sid] + dt, ok := s.data.Load(sid) if !ok { - store.reset(ctx, sid, expired, nil) - return store, nil + return newStore(ctx, s, sid, expired, nil), nil } + item := dt.(*dataItem) item.expiredAt = now().Add(time.Duration(expired) * time.Second) - for e := s.list.Front(); e != nil; e = e.Next() { - if e.Value.(*dataItem).sid == sid { - s.list.MoveToBack(e) - break - } - } - - store.reset(ctx, sid, expired, item.values) - return store, nil + s.data.Store(sid, item) + return newStore(ctx, s, sid, expired, item.values), nil } func (s *memoryStore) delete(sid string) { - delete(s.data, sid) - for e := s.list.Front(); e != nil; e = e.Next() { - if e.Value.(*dataItem).sid == sid { - s.list.Remove(e) - break - } - } + s.data.Delete(sid) } func (s *memoryStore) Delete(_ context.Context, sid string) error { - s.Lock() - defer s.Unlock() s.delete(sid) return nil } func (s *memoryStore) Refresh(ctx context.Context, oldsid, sid string, expired int64) (Store, error) { - store := s.pool.Get().(*store) - - s.Lock() - defer s.Unlock() - - item, ok := s.data[oldsid] + dt, ok := s.data.Load(oldsid) if !ok { - store.reset(ctx, sid, expired, nil) - return store, nil + return newStore(ctx, s, sid, expired, nil), nil } + item := dt.(*dataItem) newItem := newDataItem(sid, item.values, expired) - s.data[sid] = newItem - s.list.PushBack(newItem) + s.data.Store(sid, newItem) s.delete(oldsid) - - store.reset(ctx, sid, expired, newItem.values) - return store, nil + return newStore(ctx, s, sid, expired, newItem.values), nil } func (s *memoryStore) Close() error { @@ -206,29 +153,29 @@ func (s *memoryStore) Close() error { return nil } -func newStore(mstore *memoryStore) *store { - return &store{mstore: mstore} +func newStore(ctx context.Context, mstore *memoryStore, sid string, expired int64, values map[string]interface{}) *store { + if values == nil { + values = make(map[string]interface{}) + } + + return &store{ + mstore: mstore, + ctx: ctx, + sid: sid, + expired: expired, + values: values, + } } type store struct { - mstore *memoryStore sync.RWMutex + mstore *memoryStore ctx context.Context sid string expired int64 values map[string]interface{} } -func (s *store) reset(ctx context.Context, sid string, expired int64, values map[string]interface{}) { - if values == nil { - values = make(map[string]interface{}) - } - s.ctx = ctx - s.sid = sid - s.expired = expired - s.values = values -} - func (s *store) Context() context.Context { return s.ctx } diff --git a/store_test.go b/store_test.go index f8d49a5..3e20a4e 100644 --- a/store_test.go +++ b/store_test.go @@ -118,7 +118,7 @@ func testStoreWithExpired(mstore ManagerStore) { So(foo, ShouldEqual, "bar") So(ok, ShouldBeTrue) - time.Sleep(time.Second * 2) + time.Sleep(time.Second * 3) exists, err := mstore.Check(context.Background(), sid) So(err, ShouldBeNil) diff --git a/util.go b/util.go new file mode 100644 index 0000000..e654951 --- /dev/null +++ b/util.go @@ -0,0 +1,28 @@ +package session + +import ( + "crypto/rand" + "encoding/hex" + "io" +) + +// create a UUID, reference: https://github.com/google/uuid +func newUUID() string { + var buf [16]byte + _, _ = io.ReadFull(rand.Reader, buf[:]) + buf[6] = (buf[6] & 0x0f) | 0x40 + buf[8] = (buf[8] & 0x3f) | 0x80 + + dst := make([]byte, 36) + hex.Encode(dst, buf[:4]) + dst[8] = '-' + hex.Encode(dst[9:13], buf[4:6]) + dst[13] = '-' + hex.Encode(dst[14:18], buf[6:8]) + dst[18] = '-' + hex.Encode(dst[19:23], buf[8:10]) + dst[23] = '-' + hex.Encode(dst[24:], buf[10:]) + + return string(dst) +} diff --git a/uuid.go b/uuid.go deleted file mode 100644 index 2bdac7a..0000000 --- a/uuid.go +++ /dev/null @@ -1,67 +0,0 @@ -package session - -import ( - "crypto/rand" - "encoding/hex" - "io" -) - -// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC -// 4122. -type UUID [16]byte - -// Bytes returns bytes slice representation of UUID. -func (uuid UUID) Bytes() []byte { - return uuid[:] -} - -// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// , or "" if uuid is invalid. -func (uuid UUID) String() string { - var buf [36]byte - encodeHex(buf[:], uuid) - return string(buf[:]) -} - -func encodeHex(dst []byte, uuid UUID) { - hex.Encode(dst, uuid[:4]) - dst[8] = '-' - hex.Encode(dst[9:13], uuid[4:6]) - dst[13] = '-' - hex.Encode(dst[14:18], uuid[6:8]) - dst[18] = '-' - hex.Encode(dst[19:23], uuid[8:10]) - dst[23] = '-' - hex.Encode(dst[24:], uuid[10:]) -} - -// returns uuid if err is nil and panics otherwise. -func must(uuid UUID, err error) UUID { - if err != nil { - panic(err) - } - return uuid -} - -// NewRandom returns a Random (Version 4) UUID. -// -// The strength of the UUIDs is based on the strength of the crypto/rand -// package. -// -// A note about uniqueness derived from the UUID Wikipedia entry: -// -// Randomly generated UUIDs have 122 random bits. One's annual risk of being -// hit by a meteorite is estimated to be one chance in 17 billion, that -// means the probability is about 0.00000000006 (6 × 10−11), -// equivalent to the odds of creating a few tens of trillions of UUIDs in a -// year and having one duplicate. -func NewRandom() (UUID, error) { - var uuid UUID - _, err := io.ReadFull(rand.Reader, uuid[:]) - if err != nil { - return uuid, err - } - uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 - uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 - return uuid, nil -}