From 7eb45e17992a42e0ec4855386562529e73b98056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 31 Mar 2015 19:18:41 +0200 Subject: [PATCH] MySQL session: fixed problem using mysql as session store, Fixes #1681 --- CHANGELOG.md | 2 + Godeps/Godeps.json | 2 +- .../macaron-contrib/session/.gitignore | 2 + .../macaron-contrib/session/README.md | 6 +- .../macaron-contrib/session/file.go | 40 +--- .../macaron-contrib/session/ledis/ledis.go | 153 ++++++++----- .../session/ledis/ledis.goconvey | 1 + .../session/ledis/ledis_test.go | 105 +++++++++ .../session/memcache/memcache.go | 210 ++++++++--------- .../session/memcache/memcache.goconvey | 1 + .../session/memcache/memcache_test.go | 107 +++++++++ .../macaron-contrib/session/memory.go | 36 +-- .../macaron-contrib/session/mysql/mysql.go | 148 ++++++------ .../session/mysql/mysql.goconvey | 1 + .../session/mysql/mysql_test.go | 138 ++++++++++++ .../macaron-contrib/session/nodb/nodb.go | 203 +++++++++++++++++ .../session/nodb/nodb.goconvey | 1 + .../macaron-contrib/session/nodb/nodb_test.go | 105 +++++++++ .../session/postgres/postgres.go | 196 ++++++++++++++++ .../session/postgres/postgres.goconvey | 1 + .../session/postgres/postgres_test.go | 138 ++++++++++++ .../session/postgres/postgresql.go | 211 ------------------ .../macaron-contrib/session/redis/redis.go | 199 +++++++++-------- .../session/redis/redis.goconvey | 1 + .../session/redis/redis_test.go | 107 +++++++++ .../macaron-contrib/session/session.go | 44 ++-- .../macaron-contrib/session/session_test.go | 2 +- .../macaron-contrib/session/utils.go | 30 +-- 28 files changed, 1529 insertions(+), 661 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/.gitignore create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.goconvey create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis_test.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.goconvey create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache_test.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.goconvey create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql_test.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.goconvey create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb_test.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.goconvey create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres_test.go delete mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgresql.go create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.goconvey create mode 100644 Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0ab7cde47..e21e94685d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # 2.0.0-RC1 (unreleased) +**FIxes** +- [Issue #1681](https://github.com/grafana/grafana/issues/1681). MySQL session: fixed problem using mysql as session store - [Issue #1671](https://github.com/grafana/grafana/issues/1671). Data sources: Fixed issue with changing default data source (should not require full page load to take effect, now fixed) # 2.0.0-Beta1 (2015-03-30) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 857b045ddd8..ce0a9ea142e 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -47,7 +47,7 @@ }, { "ImportPath": "github.com/macaron-contrib/session", - "Rev": "65b8817c40cb5bdce08673a15fd2a648c2ba0e16" + "Rev": "31e841d95c7302b9ac456c830ea2d6dfcef4f84a" }, { "ImportPath": "github.com/mattn/go-sqlite3", diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/.gitignore b/Godeps/_workspace/src/github.com/macaron-contrib/session/.gitignore new file mode 100644 index 00000000000..9297dbcd7c4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/.gitignore @@ -0,0 +1,2 @@ +ledis/tmp.db +nodb/tmp.db \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/README.md b/Godeps/_workspace/src/github.com/macaron-contrib/session/README.md index 496ce64ce0d..01de811eacc 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/README.md +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/README.md @@ -1,7 +1,7 @@ session [![Build Status](https://drone.io/github.com/macaron-contrib/session/status.png)](https://drone.io/github.com/macaron-contrib/session/latest) [![](http://gocover.io/_badge/github.com/macaron-contrib/session)](http://gocover.io/github.com/macaron-contrib/session) ======= -Middleware session provides session management for [Macaron](https://github.com/Unknwon/macaron). It can use many session providers, including memory, file, Redis, Memcache, PostgreSQL, MySQL, Couchbase and Ledis. +Middleware session provides session management for [Macaron](https://github.com/Unknwon/macaron). It can use many session providers, including memory, file, Redis, Memcache, PostgreSQL, MySQL, Couchbase, Ledis and Nodb. ### Installation @@ -12,6 +12,10 @@ Middleware session provides session management for [Macaron](https://github.com/ - [API Reference](https://gowalker.org/github.com/macaron-contrib/session) - [Documentation](http://macaron.gogs.io/docs/middlewares/session) +## Credits + +This package is forked from [beego/session](https://github.com/astaxie/beego/tree/master/session) with reconstruction(over 80%). + ## License This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/file.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/file.go index 4dfb906ec7e..cab807d00bd 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/file.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/file.go @@ -28,17 +28,17 @@ import ( "github.com/Unknwon/com" ) -// FileSessionStore represents a file session store implementation. -type FileSessionStore struct { +// FileStore represents a file session store implementation. +type FileStore struct { p *FileProvider sid string lock sync.RWMutex data map[interface{}]interface{} } -// NewFileSessionStore creates and returns a file session store. -func NewFileSessionStore(p *FileProvider, sid string, kv map[interface{}]interface{}) *FileSessionStore { - return &FileSessionStore{ +// NewFileStore creates and returns a file session store. +func NewFileStore(p *FileProvider, sid string, kv map[interface{}]interface{}) *FileStore { + return &FileStore{ p: p, sid: sid, data: kv, @@ -46,7 +46,7 @@ func NewFileSessionStore(p *FileProvider, sid string, kv map[interface{}]interfa } // Set sets value to given key in session. -func (s *FileSessionStore) Set(key, val interface{}) error { +func (s *FileStore) Set(key, val interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -55,7 +55,7 @@ func (s *FileSessionStore) Set(key, val interface{}) error { } // Get gets value by given key in session. -func (s *FileSessionStore) Get(key interface{}) interface{} { +func (s *FileStore) Get(key interface{}) interface{} { s.lock.RLock() defer s.lock.RUnlock() @@ -63,7 +63,7 @@ func (s *FileSessionStore) Get(key interface{}) interface{} { } // Delete delete a key from session. -func (s *FileSessionStore) Delete(key interface{}) error { +func (s *FileStore) Delete(key interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -72,12 +72,12 @@ func (s *FileSessionStore) Delete(key interface{}) error { } // ID returns current session ID. -func (s *FileSessionStore) ID() string { +func (s *FileStore) ID() string { return s.sid } // Release releases resource and save data to provider. -func (s *FileSessionStore) Release() error { +func (s *FileStore) Release() error { data, err := EncodeGob(s.data) if err != nil { return err @@ -87,7 +87,7 @@ func (s *FileSessionStore) Release() error { } // Flush deletes all session data. -func (s *FileSessionStore) Flush() error { +func (s *FileStore) Flush() error { s.lock.Lock() defer s.lock.Unlock() @@ -97,7 +97,6 @@ func (s *FileSessionStore) Flush() error { // FileProvider represents a file session provider implementation. type FileProvider struct { - lock sync.RWMutex maxlifetime int64 rootPath string } @@ -115,9 +114,6 @@ func (p *FileProvider) filepath(sid string) string { // Read returns raw session store by session ID. func (p *FileProvider) Read(sid string) (_ RawStore, err error) { - p.lock.Lock() - defer p.lock.Unlock() - filename := p.filepath(sid) if err = os.MkdirAll(path.Dir(filename), os.ModePerm); err != nil { return nil, err @@ -151,22 +147,16 @@ func (p *FileProvider) Read(sid string) (_ RawStore, err error) { return nil, err } } - return NewFileSessionStore(p, sid, kv), nil + return NewFileStore(p, sid, kv), nil } // Exist returns true if session with given ID exists. func (p *FileProvider) Exist(sid string) bool { - p.lock.Lock() - defer p.lock.Unlock() - return com.IsFile(p.filepath(sid)) } // Destory deletes a session by session ID. func (p *FileProvider) Destory(sid string) error { - p.lock.Lock() - defer p.lock.Unlock() - return os.Remove(p.filepath(sid)) } @@ -201,12 +191,9 @@ func (p *FileProvider) regenerate(oldsid, sid string) (err error) { // Regenerate regenerates a session store from old session ID to new one. func (p *FileProvider) Regenerate(oldsid, sid string) (_ RawStore, err error) { - p.lock.Lock() if err := p.regenerate(oldsid, sid); err != nil { - p.lock.Unlock() return nil, err } - p.lock.Unlock() return p.Read(sid) } @@ -236,9 +223,6 @@ func (p *FileProvider) GC() { return } - p.lock.Lock() - defer p.lock.Unlock() - if err := filepath.Walk(p.rootPath, func(path string, fi os.FileInfo, err error) error { if err != nil { return err diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.go index 7893769b525..afde7134cbd 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.go @@ -16,26 +16,39 @@ package session import ( + "fmt" + "strings" "sync" + "github.com/Unknwon/com" "github.com/siddontang/ledisdb/config" "github.com/siddontang/ledisdb/ledis" + "gopkg.in/ini.v1" "github.com/macaron-contrib/session" ) -var c *ledis.DB +// LedisStore represents a ledis session store implementation. +type LedisStore struct { + c *ledis.DB + sid string + expire int64 + lock sync.RWMutex + data map[interface{}]interface{} +} -// LedisSessionStore represents a ledis session store implementation. -type LedisSessionStore struct { - sid string - lock sync.RWMutex - data map[interface{}]interface{} - maxlifetime int64 +// NewLedisStore creates and returns a ledis session store. +func NewLedisStore(c *ledis.DB, sid string, expire int64, kv map[interface{}]interface{}) *LedisStore { + return &LedisStore{ + c: c, + expire: expire, + sid: sid, + data: kv, + } } // Set sets value to given key in session. -func (s *LedisSessionStore) Set(key, val interface{}) error { +func (s *LedisStore) Set(key, val interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -44,7 +57,7 @@ func (s *LedisSessionStore) Set(key, val interface{}) error { } // Get gets value by given key in session. -func (s *LedisSessionStore) Get(key interface{}) interface{} { +func (s *LedisStore) Get(key interface{}) interface{} { s.lock.RLock() defer s.lock.RUnlock() @@ -52,7 +65,7 @@ func (s *LedisSessionStore) Get(key interface{}) interface{} { } // Delete delete a key from session. -func (s *LedisSessionStore) Delete(key interface{}) error { +func (s *LedisStore) Delete(key interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -61,25 +74,26 @@ func (s *LedisSessionStore) Delete(key interface{}) error { } // ID returns current session ID. -func (s *LedisSessionStore) ID() string { +func (s *LedisStore) ID() string { return s.sid } // Release releases resource and save data to provider. -func (s *LedisSessionStore) Release() error { +func (s *LedisStore) Release() error { data, err := session.EncodeGob(s.data) if err != nil { return err } - if err = c.Set([]byte(s.sid), data); err != nil { + + if err = s.c.Set([]byte(s.sid), data); err != nil { return err } - _, err = c.Expire([]byte(s.sid), s.maxlifetime) + _, err = s.c.Expire([]byte(s.sid), s.expire) return err } // Flush deletes all session data. -func (s *LedisSessionStore) Flush() error { +func (s *LedisStore) Flush() error { s.lock.Lock() defer s.lock.Unlock() @@ -89,30 +103,54 @@ func (s *LedisSessionStore) Flush() error { // LedisProvider represents a ledis session provider implementation. type LedisProvider struct { - maxlifetime int64 - savePath string + c *ledis.DB + expire int64 } -// Init initializes memory session provider. -func (p *LedisProvider) Init(maxlifetime int64, savePath string) error { - p.maxlifetime = maxlifetime - p.savePath = savePath - cfg := new(config.Config) - cfg.DataDir = p.savePath - var err error - nowLedis, err := ledis.Open(cfg) - c, err = nowLedis.Select(0) +// Init initializes ledis session provider. +// configs: data_dir=./app.db,db=0 +func (p *LedisProvider) Init(expire int64, configs string) error { + p.expire = expire + + cfg, err := ini.Load([]byte(strings.Replace(configs, ",", "\n", -1))) if err != nil { - println(err) - return nil + return err } - return nil + + db := 0 + opt := new(config.Config) + for k, v := range cfg.Section("").KeysHash() { + switch k { + case "data_dir": + opt.DataDir = v + case "db": + db = com.StrTo(v).MustInt() + default: + return fmt.Errorf("session/ledis: unsupported option '%s'", k) + } + } + + l, err := ledis.Open(opt) + if err != nil { + return fmt.Errorf("session/ledis: error opening db: %v", err) + } + p.c, err = l.Select(db) + return err } // Read returns raw session store by session ID. func (p *LedisProvider) Read(sid string) (session.RawStore, error) { - kvs, err := c.Get([]byte(sid)) + if !p.Exist(sid) { + if err := p.c.Set([]byte(sid), []byte("")); err != nil { + return nil, err + } + } + var kv map[interface{}]interface{} + kvs, err := p.c.Get([]byte(sid)) + if err != nil { + return nil, err + } if len(kvs) == 0 { kv = make(map[interface{}]interface{}) } else { @@ -121,41 +159,40 @@ func (p *LedisProvider) Read(sid string) (session.RawStore, error) { return nil, err } } - ls := &LedisSessionStore{sid: sid, data: kv, maxlifetime: p.maxlifetime} - return ls, nil + + return NewLedisStore(p.c, sid, p.expire, kv), nil } // Exist returns true if session with given ID exists. func (p *LedisProvider) Exist(sid string) bool { - count, _ := c.Exists([]byte(sid)) - if count == 0 { - return false - } else { - return true - } + count, err := p.c.Exists([]byte(sid)) + return err == nil && count > 0 } // Destory deletes a session by session ID. func (p *LedisProvider) Destory(sid string) error { - _, err := c.Del([]byte(sid)) + _, err := p.c.Del([]byte(sid)) return err } // Regenerate regenerates a session store from old session ID to new one. -func (p *LedisProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { - count, _ := c.Exists([]byte(sid)) - if count == 0 { - // oldsid doesn't exists, set the new sid directly - // ignore error here, since if it return error - // the existed value will be 0 - c.Set([]byte(sid), []byte("")) - c.Expire([]byte(sid), p.maxlifetime) - } else { - data, _ := c.Get([]byte(oldsid)) - c.Set([]byte(sid), data) - c.Expire([]byte(sid), p.maxlifetime) +func (p *LedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) } - kvs, err := c.Get([]byte(sid)) + + kvs := make([]byte, 0) + if p.Exist(oldsid) { + if kvs, err = p.c.Get([]byte(oldsid)); err != nil { + return nil, err + } else if _, err = p.c.Del([]byte(oldsid)); err != nil { + return nil, err + } + } + if err = p.c.SetEX([]byte(sid), p.expire, kvs); err != nil { + return nil, err + } + var kv map[interface{}]interface{} if len(kvs) == 0 { kv = make(map[interface{}]interface{}) @@ -165,18 +202,20 @@ func (p *LedisProvider) Regenerate(oldsid, sid string) (session.RawStore, error) return nil, err } } - ls := &LedisSessionStore{sid: sid, data: kv, maxlifetime: p.maxlifetime} - return ls, nil + + return NewLedisStore(p.c, sid, p.expire, kv), nil } // Count counts and returns number of sessions. func (p *LedisProvider) Count() int { - // FIXME - return 0 + // FIXME: how come this library does not have DbSize() method? + return -1 } // GC calls GC to clean expired sessions. -func (p *LedisProvider) GC() {} +func (p *LedisProvider) GC() { + // FIXME: wtf??? +} func init() { session.Register("ledis", &LedisProvider{}) diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.goconvey b/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.goconvey new file mode 100644 index 00000000000..8485e986e45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis.goconvey @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis_test.go new file mode 100644 index 00000000000..dac42a364b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/ledis/ledis_test.go @@ -0,0 +1,105 @@ +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Unknwon/macaron" + . "github.com/smartystreets/goconvey/convey" + + "github.com/macaron-contrib/session" +) + +func Test_LedisProvider(t *testing.T) { + Convey("Test ledis session provider", t, func() { + opt := session.Options{ + Provider: "ledis", + ProviderConfig: "data_dir=./tmp.db", + } + + Convey("Basic operation", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + sess.Set("uname", "unknwon") + }) + m.Get("/reg", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := raw.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + }) + m.Get("/get", func(ctx *macaron.Context, sess session.Store) { + sid := sess.ID() + So(sid, ShouldNotBeEmpty) + + raw, err := sess.Read(sid) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Delete("uname"), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + + cookie := resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/reg", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + cookie = resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/get", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + Convey("Regenrate empty session", func() { + m.Get("/empty", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + }) + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/empty", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", "MacaronSession=ad2c7e3cbecfcf486; Path=/;") + m.ServeHTTP(resp, req) + }) + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.go index e06895202f0..b4fcdde62bd 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.go @@ -16,6 +16,7 @@ package session import ( + "fmt" "strings" "sync" @@ -24,20 +25,35 @@ import ( "github.com/macaron-contrib/session" ) -var ( - client *memcache.Client -) +// MemcacheStore represents a memcache session store implementation. +type MemcacheStore struct { + c *memcache.Client + sid string + expire int32 + lock sync.RWMutex + data map[interface{}]interface{} +} -// MemcacheSessionStore represents a memcache session store implementation. -type MemcacheSessionStore struct { - sid string - lock sync.RWMutex - data map[interface{}]interface{} - maxlifetime int64 +// NewMemcacheStore creates and returns a memcache session store. +func NewMemcacheStore(c *memcache.Client, sid string, expire int32, kv map[interface{}]interface{}) *MemcacheStore { + return &MemcacheStore{ + c: c, + sid: sid, + expire: expire, + data: kv, + } +} + +func NewItem(sid string, data []byte, expire int32) *memcache.Item { + return &memcache.Item{ + Key: sid, + Value: data, + Expiration: expire, + } } // Set sets value to given key in session. -func (s *MemcacheSessionStore) Set(key, val interface{}) error { +func (s *MemcacheStore) Set(key, val interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -46,7 +62,7 @@ func (s *MemcacheSessionStore) Set(key, val interface{}) error { } // Get gets value by given key in session. -func (s *MemcacheSessionStore) Get(key interface{}) interface{} { +func (s *MemcacheStore) Get(key interface{}) interface{} { s.lock.RLock() defer s.lock.RUnlock() @@ -54,7 +70,7 @@ func (s *MemcacheSessionStore) Get(key interface{}) interface{} { } // Delete delete a key from session. -func (s *MemcacheSessionStore) Delete(key interface{}) error { +func (s *MemcacheStore) Delete(key interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -63,26 +79,22 @@ func (s *MemcacheSessionStore) Delete(key interface{}) error { } // ID returns current session ID. -func (s *MemcacheSessionStore) ID() string { +func (s *MemcacheStore) ID() string { return s.sid } // Release releases resource and save data to provider. -func (s *MemcacheSessionStore) Release() error { +func (s *MemcacheStore) Release() error { data, err := session.EncodeGob(s.data) if err != nil { return err } - return client.Set(&memcache.Item{ - Key: s.sid, - Value: data, - Expiration: int32(s.maxlifetime), - }) + return s.c.Set(NewItem(s.sid, data, s.expire)) } // Flush deletes all session data. -func (s *MemcacheSessionStore) Flush() error { +func (s *MemcacheStore) Flush() error { s.lock.Lock() defer s.lock.Unlock() @@ -90,41 +102,75 @@ func (s *MemcacheSessionStore) Flush() error { return nil } -// MemProvider represents a memcache session provider implementation. -type MemProvider struct { - maxlifetime int64 - conninfo []string - poolsize int - password string +// MemcacheProvider represents a memcache session provider implementation. +type MemcacheProvider struct { + c *memcache.Client + expire int32 } -// Init initializes memory session provider. -// connStrs can be multiple connection strings separate by ; -// e.g. 127.0.0.1:9090 -func (p *MemProvider) Init(maxlifetime int64, connStrs string) error { - p.maxlifetime = maxlifetime - p.conninfo = strings.Split(connStrs, ";") - client = memcache.New(p.conninfo...) - return nil -} - -func (p *MemProvider) connectInit() error { - client = memcache.New(p.conninfo...) +// Init initializes memcache session provider. +// connStrs: 127.0.0.1:9090;127.0.0.1:9091 +func (p *MemcacheProvider) Init(expire int64, connStrs string) error { + p.expire = int32(expire) + p.c = memcache.New(strings.Split(connStrs, ";")...) return nil } // Read returns raw session store by session ID. -func (p *MemProvider) Read(sid string) (session.RawStore, error) { - if client == nil { - if err := p.connectInit(); err != nil { +func (p *MemcacheProvider) Read(sid string) (session.RawStore, error) { + if !p.Exist(sid) { + if err := p.c.Set(NewItem(sid, []byte(""), p.expire)); err != nil { return nil, err } } - item, err := client.Get(sid) + var kv map[interface{}]interface{} + item, err := p.c.Get(sid) if err != nil { return nil, err } + if len(item.Value) == 0 { + kv = make(map[interface{}]interface{}) + } else { + kv, err = session.DecodeGob(item.Value) + if err != nil { + return nil, err + } + } + + return NewMemcacheStore(p.c, sid, p.expire, kv), nil +} + +// Exist returns true if session with given ID exists. +func (p *MemcacheProvider) Exist(sid string) bool { + _, err := p.c.Get(sid) + return err == nil +} + +// Destory deletes a session by session ID. +func (p *MemcacheProvider) Destory(sid string) error { + return p.c.Delete(sid) +} + +// Regenerate regenerates a session store from old session ID to new one. +func (p *MemcacheProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) + } + + item := NewItem(sid, []byte(""), p.expire) + if p.Exist(oldsid) { + item, err = p.c.Get(oldsid) + if err != nil { + return nil, err + } else if err = p.c.Delete(oldsid); err != nil { + return nil, err + } + item.Key = sid + } + if err = p.c.Set(item); err != nil { + return nil, err + } var kv map[interface{}]interface{} if len(item.Value) == 0 { @@ -136,86 +182,18 @@ func (p *MemProvider) Read(sid string) (session.RawStore, error) { } } - rs := &MemcacheSessionStore{sid: sid, data: kv, maxlifetime: p.maxlifetime} - return rs, nil -} - -// Exist returns true if session with given ID exists. -func (p *MemProvider) Exist(sid string) bool { - if client == nil { - if err := p.connectInit(); err != nil { - return false - } - } - - if item, err := client.Get(sid); err != nil || len(item.Value) == 0 { - return false - } else { - return true - } -} - -// Destory deletes a session by session ID. -func (p *MemProvider) Destory(sid string) error { - if client == nil { - if err := p.connectInit(); err != nil { - return err - } - } - - return client.Delete(sid) -} - -// Regenerate regenerates a session store from old session ID to new one. -func (p *MemProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { - if client == nil { - if err := p.connectInit(); err != nil { - return nil, err - } - } - - var contain []byte - if item, err := client.Get(sid); err != nil || len(item.Value) == 0 { - // oldsid doesn't exists, set the new sid directly - // ignore error here, since if it return error - // the existed value will be 0 - item.Key = sid - item.Value = []byte("") - item.Expiration = int32(p.maxlifetime) - client.Set(item) - } else { - client.Delete(oldsid) - item.Key = sid - item.Value = item.Value - item.Expiration = int32(p.maxlifetime) - client.Set(item) - contain = item.Value - } - - var kv map[interface{}]interface{} - if len(contain) == 0 { - kv = make(map[interface{}]interface{}) - } else { - var err error - kv, err = session.DecodeGob(contain) - if err != nil { - return nil, err - } - } - - rs := &MemcacheSessionStore{sid: sid, data: kv, maxlifetime: p.maxlifetime} - return rs, nil + return NewMemcacheStore(p.c, sid, p.expire, kv), nil } // Count counts and returns number of sessions. -func (p *MemProvider) Count() int { - // FIXME - return 0 +func (p *MemcacheProvider) Count() int { + // FIXME: how come this library does not have Stats method? + return -1 } // GC calls GC to clean expired sessions. -func (p *MemProvider) GC() {} +func (p *MemcacheProvider) GC() {} func init() { - session.Register("memcache", &MemProvider{}) + session.Register("memcache", &MemcacheProvider{}) } diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.goconvey b/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.goconvey new file mode 100644 index 00000000000..8485e986e45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache.goconvey @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache_test.go new file mode 100644 index 00000000000..beb272d52b1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/memcache/memcache_test.go @@ -0,0 +1,107 @@ +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Unknwon/macaron" + . "github.com/smartystreets/goconvey/convey" + + "github.com/macaron-contrib/session" +) + +func Test_MemcacheProvider(t *testing.T) { + Convey("Test memcache session provider", t, func() { + opt := session.Options{ + Provider: "memcache", + ProviderConfig: "127.0.0.1:9090", + } + + Convey("Basic operation", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + sess.Set("uname", "unknwon") + }) + m.Get("/reg", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := raw.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + }) + m.Get("/get", func(ctx *macaron.Context, sess session.Store) { + sid := sess.ID() + So(sid, ShouldNotBeEmpty) + + raw, err := sess.Read(sid) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Delete("uname"), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + + cookie := resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/reg", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + cookie = resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/get", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + }) + + Convey("Regenrate empty session", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", "MacaronSession=ad2c7e3cbecfcf486; Path=/;") + m.ServeHTTP(resp, req) + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/memory.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/memory.go index 040b8e697dc..e717635b951 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/memory.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/memory.go @@ -22,17 +22,17 @@ import ( "time" ) -// MemSessionStore represents a in-memory session store implementation. -type MemSessionStore struct { +// MemStore represents a in-memory session store implementation. +type MemStore struct { sid string lock sync.RWMutex data map[interface{}]interface{} lastAccess time.Time } -// NewMemSessionStore creates and returns a memory session store. -func NewMemSessionStore(sid string) *MemSessionStore { - return &MemSessionStore{ +// NewMemStore creates and returns a memory session store. +func NewMemStore(sid string) *MemStore { + return &MemStore{ sid: sid, data: make(map[interface{}]interface{}), lastAccess: time.Now(), @@ -40,7 +40,7 @@ func NewMemSessionStore(sid string) *MemSessionStore { } // Set sets value to given key in session. -func (s *MemSessionStore) Set(key, val interface{}) error { +func (s *MemStore) Set(key, val interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -49,15 +49,15 @@ func (s *MemSessionStore) Set(key, val interface{}) error { } // Get gets value by given key in session. -func (s *MemSessionStore) Get(key interface{}) interface{} { +func (s *MemStore) Get(key interface{}) interface{} { s.lock.RLock() defer s.lock.RUnlock() return s.data[key] } -// Delete delete a key from session. -func (s *MemSessionStore) Delete(key interface{}) error { +// Delete deletes a key from session. +func (s *MemStore) Delete(key interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -66,17 +66,17 @@ func (s *MemSessionStore) Delete(key interface{}) error { } // ID returns current session ID. -func (s *MemSessionStore) ID() string { +func (s *MemStore) ID() string { return s.sid } // Release releases resource and save data to provider. -func (_ *MemSessionStore) Release() error { +func (_ *MemStore) Release() error { return nil } // Flush deletes all session data. -func (s *MemSessionStore) Flush() error { +func (s *MemStore) Flush() error { s.lock.Lock() defer s.lock.Unlock() @@ -105,7 +105,7 @@ func (p *MemProvider) update(sid string) error { defer p.lock.Unlock() if e, ok := p.data[sid]; ok { - e.Value.(*MemSessionStore).lastAccess = time.Now() + e.Value.(*MemStore).lastAccess = time.Now() p.list.MoveToFront(e) return nil } @@ -122,14 +122,14 @@ func (p *MemProvider) Read(sid string) (_ RawStore, err error) { if err = p.update(sid); err != nil { return nil, err } - return e.Value.(*MemSessionStore), nil + return e.Value.(*MemStore), nil } // Create a new session. p.lock.Lock() defer p.lock.Unlock() - s := NewMemSessionStore(sid) + s := NewMemStore(sid) p.data[sid] = p.list.PushBack(s) return s, nil } @@ -173,7 +173,7 @@ func (p *MemProvider) Regenerate(oldsid, sid string) (RawStore, error) { return nil, err } - s.(*MemSessionStore).sid = sid + s.(*MemStore).sid = sid p.data[sid] = p.list.PushBack(s) return s, nil } @@ -193,11 +193,11 @@ func (p *MemProvider) GC() { break } - if (e.Value.(*MemSessionStore).lastAccess.Unix() + p.maxLifetime) < time.Now().Unix() { + if (e.Value.(*MemStore).lastAccess.Unix() + p.maxLifetime) < time.Now().Unix() { p.lock.RUnlock() p.lock.Lock() p.list.Remove(e) - delete(p.data, e.Value.(*MemSessionStore).sid) + delete(p.data, e.Value.(*MemStore).sid) p.lock.Unlock() p.lock.RLock() } else { diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.go index 908de4f1b52..7997e03c0d8 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.go @@ -17,6 +17,8 @@ package session import ( "database/sql" + "fmt" + "log" "sync" "time" @@ -25,16 +27,25 @@ import ( "github.com/macaron-contrib/session" ) -// MysqlSessionStore represents a mysql session store implementation. -type MysqlSessionStore struct { +// MysqlStore represents a mysql session store implementation. +type MysqlStore struct { c *sql.DB sid string lock sync.RWMutex data map[interface{}]interface{} } +// NewMysqlStore creates and returns a mysql session store. +func NewMysqlStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *MysqlStore { + return &MysqlStore{ + c: c, + sid: sid, + data: kv, + } +} + // Set sets value to given key in session. -func (s *MysqlSessionStore) Set(key, val interface{}) error { +func (s *MysqlStore) Set(key, val interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -43,7 +54,7 @@ func (s *MysqlSessionStore) Set(key, val interface{}) error { } // Get gets value by given key in session. -func (s *MysqlSessionStore) Get(key interface{}) interface{} { +func (s *MysqlStore) Get(key interface{}) interface{} { s.lock.RLock() defer s.lock.RUnlock() @@ -51,7 +62,7 @@ func (s *MysqlSessionStore) Get(key interface{}) interface{} { } // Delete delete a key from session. -func (s *MysqlSessionStore) Delete(key interface{}) error { +func (s *MysqlStore) Delete(key interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -60,24 +71,24 @@ func (s *MysqlSessionStore) Delete(key interface{}) error { } // ID returns current session ID. -func (s *MysqlSessionStore) ID() string { +func (s *MysqlStore) ID() string { return s.sid } // Release releases resource and save data to provider. -func (s *MysqlSessionStore) Release() error { - defer s.c.Close() +func (s *MysqlStore) Release() error { data, err := session.EncodeGob(s.data) if err != nil { return err } - _, err = s.c.Exec("UPDATE session set `session_data`=?, `session_expiry`=? where session_key=?", + + _, err = s.c.Exec("UPDATE session SET data=?, expiry=? WHERE `key`=?", data, time.Now().Unix(), s.sid) return err } // Flush deletes all session data. -func (s *MysqlSessionStore) Flush() error { +func (s *MysqlStore) Flush() error { s.lock.Lock() defer s.lock.Unlock() @@ -87,113 +98,96 @@ func (s *MysqlSessionStore) Flush() error { // MysqlProvider represents a mysql session provider implementation. type MysqlProvider struct { - maxlifetime int64 - connStr string + c *sql.DB + expire int64 } -func (p *MysqlProvider) connectInit() *sql.DB { - db, e := sql.Open("mysql", p.connStr) - if e != nil { - return nil +// Init initializes mysql session provider. +// connStr: username:password@protocol(address)/dbname?param=value +func (p *MysqlProvider) Init(expire int64, connStr string) (err error) { + p.expire = expire + + p.c, err = sql.Open("mysql", connStr) + if err != nil { + return err } - return db -} - -// Init initializes memory session provider. -func (p *MysqlProvider) Init(maxlifetime int64, connStr string) error { - p.maxlifetime = maxlifetime - p.connStr = connStr - return nil + return p.c.Ping() } // Read returns raw session store by session ID. func (p *MysqlProvider) Read(sid string) (session.RawStore, error) { - c := p.connectInit() - row := c.QueryRow("select session_data from session where session_key=?", sid) - var sessiondata []byte - err := row.Scan(&sessiondata) + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) if err == sql.ErrNoRows { - c.Exec("insert into session(`session_key`,`session_data`,`session_expiry`) values(?,?,?)", + _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", sid, "", time.Now().Unix()) } + if err != nil { + return nil, err + } + var kv map[interface{}]interface{} - if len(sessiondata) == 0 { + if len(data) == 0 { kv = make(map[interface{}]interface{}) } else { - kv, err = session.DecodeGob(sessiondata) + kv, err = session.DecodeGob(data) if err != nil { return nil, err } } - rs := &MysqlSessionStore{c: c, sid: sid, data: kv} - return rs, nil + + return NewMysqlStore(p.c, sid, kv), nil } // Exist returns true if session with given ID exists. func (p *MysqlProvider) Exist(sid string) bool { - c := p.connectInit() - defer c.Close() - - row := c.QueryRow("select session_data from session where session_key=?", sid) - var sessiondata []byte - err := row.Scan(&sessiondata) - if err == sql.ErrNoRows { - return false - } else { - return true + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE `key`=?", sid).Scan(&data) + if err != nil && err != sql.ErrNoRows { + panic("session/mysql: error checking existence: " + err.Error()) } + return err != sql.ErrNoRows } // Destory deletes a session by session ID. -func (p *MysqlProvider) Destory(sid string) (err error) { - c := p.connectInit() - if _, err = c.Exec("DELETE FROM session where session_key=?", sid); err != nil { - return err - } - return c.Close() +func (p *MysqlProvider) Destory(sid string) error { + _, err := p.c.Exec("DELETE FROM session WHERE `key`=?", sid) + return err } // Regenerate regenerates a session store from old session ID to new one. -func (p *MysqlProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { - c := p.connectInit() - row := c.QueryRow("select session_data from session where session_key=?", oldsid) - var sessiondata []byte - err := row.Scan(&sessiondata) - if err == sql.ErrNoRows { - c.Exec("insert into session(`session_key`,`session_data`,`session_expiry`) values(?,?,?)", oldsid, "", time.Now().Unix()) +func (p *MysqlProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) } - c.Exec("update session set `session_key`=? where session_key=?", sid, oldsid) - var kv map[interface{}]interface{} - if len(sessiondata) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob(sessiondata) - if err != nil { + + if !p.Exist(oldsid) { + if _, err = p.c.Exec("INSERT INTO session(`key`,data,expiry) VALUES(?,?,?)", + oldsid, "", time.Now().Unix()); err != nil { return nil, err } } - rs := &MysqlSessionStore{c: c, sid: sid, data: kv} - return rs, nil + + if _, err = p.c.Exec("UPDATE session SET `key`=? WHERE `key`=?", sid, oldsid); err != nil { + return nil, err + } + + return p.Read(sid) } // Count counts and returns number of sessions. -func (p *MysqlProvider) Count() int { - c := p.connectInit() - defer c.Close() - - var total int - err := c.QueryRow("SELECT count(*) as num from session").Scan(&total) - if err != nil { - return 0 +func (p *MysqlProvider) Count() (total int) { + if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { + panic("session/mysql: error counting records: " + err.Error()) } return total } // GC calls GC to clean expired sessions. -func (mp *MysqlProvider) GC() { - c := mp.connectInit() - c.Exec("DELETE from session where session_expiry < ?", time.Now().Unix()-mp.maxlifetime) - c.Close() +func (p *MysqlProvider) GC() { + if _, err := p.c.Exec("DELETE FROM session WHERE UNIX_TIMESTAMP(NOW()) - expiry > ?", p.expire); err != nil { + log.Printf("session/mysql: error garbage collecting: %v", err) + } } func init() { diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.goconvey b/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.goconvey new file mode 100644 index 00000000000..8485e986e45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql.goconvey @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql_test.go new file mode 100644 index 00000000000..15b3996a228 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/mysql/mysql_test.go @@ -0,0 +1,138 @@ +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Unknwon/macaron" + . "github.com/smartystreets/goconvey/convey" + + "github.com/macaron-contrib/session" +) + +func Test_MysqlProvider(t *testing.T) { + Convey("Test mysql session provider", t, func() { + opt := session.Options{ + Provider: "mysql", + ProviderConfig: "root:@tcp(localhost:3306)/macaron?charset=utf8", + } + + Convey("Basic operation", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + sess.Set("uname", "unknwon") + }) + m.Get("/reg", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := raw.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + }) + m.Get("/get", func(ctx *macaron.Context, sess session.Store) { + sid := sess.ID() + So(sid, ShouldNotBeEmpty) + + raw, err := sess.Read(sid) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + So(raw.Release(), ShouldBeNil) + + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Delete("uname"), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + + cookie := resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/reg", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + cookie = resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/get", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + }) + + Convey("Regenrate empty session", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", "MacaronSession=ad2c7e3cbecfcf48; Path=/;") + m.ServeHTTP(resp, req) + }) + + Convey("GC session", func() { + m := macaron.New() + opt2 := opt + opt2.Gclifetime = 1 + m.Use(session.Sessioner(opt2)) + + m.Get("/", func(sess session.Store) { + sess.Set("uname", "unknwon") + So(sess.ID(), ShouldNotBeEmpty) + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Flush(), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + time.Sleep(2 * time.Second) + sess.GC() + So(sess.Count(), ShouldEqual, 0) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.go new file mode 100644 index 00000000000..7f017bf0457 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.go @@ -0,0 +1,203 @@ +// Copyright 2015 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "fmt" + "sync" + + "github.com/lunny/nodb" + "github.com/lunny/nodb/config" + + "github.com/macaron-contrib/session" +) + +// NodbStore represents a nodb session store implementation. +type NodbStore struct { + c *nodb.DB + sid string + expire int64 + lock sync.RWMutex + data map[interface{}]interface{} +} + +// NewNodbStore creates and returns a ledis session store. +func NewNodbStore(c *nodb.DB, sid string, expire int64, kv map[interface{}]interface{}) *NodbStore { + return &NodbStore{ + c: c, + expire: expire, + sid: sid, + data: kv, + } +} + +// Set sets value to given key in session. +func (s *NodbStore) Set(key, val interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data[key] = val + return nil +} + +// Get gets value by given key in session. +func (s *NodbStore) Get(key interface{}) interface{} { + s.lock.RLock() + defer s.lock.RUnlock() + + return s.data[key] +} + +// Delete delete a key from session. +func (s *NodbStore) Delete(key interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + delete(s.data, key) + return nil +} + +// ID returns current session ID. +func (s *NodbStore) ID() string { + return s.sid +} + +// Release releases resource and save data to provider. +func (s *NodbStore) Release() error { + data, err := session.EncodeGob(s.data) + if err != nil { + return err + } + + if err = s.c.Set([]byte(s.sid), data); err != nil { + return err + } + _, err = s.c.Expire([]byte(s.sid), s.expire) + return err +} + +// Flush deletes all session data. +func (s *NodbStore) Flush() error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = make(map[interface{}]interface{}) + return nil +} + +// NodbProvider represents a ledis session provider implementation. +type NodbProvider struct { + c *nodb.DB + expire int64 +} + +// Init initializes nodb session provider. +func (p *NodbProvider) Init(expire int64, configs string) error { + p.expire = expire + + cfg := new(config.Config) + cfg.DataDir = configs + dbs, err := nodb.Open(cfg) + if err != nil { + return fmt.Errorf("session/nodb: error opening db: %v", err) + } + + p.c, err = dbs.Select(0) + return err +} + +// Read returns raw session store by session ID. +func (p *NodbProvider) Read(sid string) (session.RawStore, error) { + if !p.Exist(sid) { + if err := p.c.Set([]byte(sid), []byte("")); err != nil { + return nil, err + } + } + + var kv map[interface{}]interface{} + kvs, err := p.c.Get([]byte(sid)) + if err != nil { + return nil, err + } + if len(kvs) == 0 { + kv = make(map[interface{}]interface{}) + } else { + kv, err = session.DecodeGob(kvs) + if err != nil { + return nil, err + } + } + + return NewNodbStore(p.c, sid, p.expire, kv), nil +} + +// Exist returns true if session with given ID exists. +func (p *NodbProvider) Exist(sid string) bool { + count, err := p.c.Exists([]byte(sid)) + return err == nil && count > 0 +} + +// Destory deletes a session by session ID. +func (p *NodbProvider) Destory(sid string) error { + _, err := p.c.Del([]byte(sid)) + return err +} + +// Regenerate regenerates a session store from old session ID to new one. +func (p *NodbProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) + } + + kvs := make([]byte, 0) + if p.Exist(oldsid) { + if kvs, err = p.c.Get([]byte(oldsid)); err != nil { + return nil, err + } else if _, err = p.c.Del([]byte(oldsid)); err != nil { + return nil, err + } + } + + if err = p.c.Set([]byte(sid), kvs); err != nil { + return nil, err + } else if _, err = p.c.Expire([]byte(sid), p.expire); err != nil { + return nil, err + } + + var kv map[interface{}]interface{} + if len(kvs) == 0 { + kv = make(map[interface{}]interface{}) + } else { + kv, err = session.DecodeGob([]byte(kvs)) + if err != nil { + return nil, err + } + } + + return NewNodbStore(p.c, sid, p.expire, kv), nil +} + +// Count counts and returns number of sessions. +func (p *NodbProvider) Count() int { + // FIXME: how come this library does not have DbSize() method? + return -1 +} + +// GC calls GC to clean expired sessions. +func (p *NodbProvider) GC() {} + +func init() { + session.Register("nodb", &NodbProvider{}) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.goconvey b/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.goconvey new file mode 100644 index 00000000000..8485e986e45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb.goconvey @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb_test.go new file mode 100644 index 00000000000..c86ba98ded5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/nodb/nodb_test.go @@ -0,0 +1,105 @@ +// Copyright 2015 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Unknwon/macaron" + . "github.com/smartystreets/goconvey/convey" + + "github.com/macaron-contrib/session" +) + +func Test_LedisProvider(t *testing.T) { + Convey("Test nodb session provider", t, func() { + opt := session.Options{ + Provider: "nodb", + ProviderConfig: "./tmp.db", + } + + Convey("Basic operation", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + sess.Set("uname", "unknwon") + }) + m.Get("/reg", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := raw.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + }) + m.Get("/get", func(ctx *macaron.Context, sess session.Store) { + sid := sess.ID() + So(sid, ShouldNotBeEmpty) + + raw, err := sess.Read(sid) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Delete("uname"), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + + cookie := resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/reg", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + cookie = resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/get", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + Convey("Regenrate empty session", func() { + m.Get("/empty", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + }) + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/empty", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", "MacaronSession=ad2c7e3cbecfcf486; Path=/;") + m.ServeHTTP(resp, req) + }) + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.go new file mode 100644 index 00000000000..5cb4c82ea84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.go @@ -0,0 +1,196 @@ +// Copyright 2013 Beego Authors +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "database/sql" + "fmt" + "log" + "sync" + "time" + + _ "github.com/lib/pq" + + "github.com/macaron-contrib/session" +) + +// PostgresStore represents a postgres session store implementation. +type PostgresStore struct { + c *sql.DB + sid string + lock sync.RWMutex + data map[interface{}]interface{} +} + +// NewPostgresStore creates and returns a postgres session store. +func NewPostgresStore(c *sql.DB, sid string, kv map[interface{}]interface{}) *PostgresStore { + return &PostgresStore{ + c: c, + sid: sid, + data: kv, + } +} + +// Set sets value to given key in session. +func (s *PostgresStore) Set(key, value interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data[key] = value + return nil +} + +// Get gets value by given key in session. +func (s *PostgresStore) Get(key interface{}) interface{} { + s.lock.RLock() + defer s.lock.RUnlock() + + return s.data[key] +} + +// Delete delete a key from session. +func (s *PostgresStore) Delete(key interface{}) error { + s.lock.Lock() + defer s.lock.Unlock() + + delete(s.data, key) + return nil +} + +// ID returns current session ID. +func (s *PostgresStore) ID() string { + return s.sid +} + +// save postgres session values to database. +// must call this method to save values to database. +func (s *PostgresStore) Release() error { + data, err := session.EncodeGob(s.data) + if err != nil { + return err + } + + _, err = s.c.Exec("UPDATE session SET data=$1, expiry=$2 WHERE key=$3", + data, time.Now().Unix(), s.sid) + return err +} + +// Flush deletes all session data. +func (s *PostgresStore) Flush() error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = make(map[interface{}]interface{}) + return nil +} + +// PostgresProvider represents a postgres session provider implementation. +type PostgresProvider struct { + c *sql.DB + maxlifetime int64 +} + +// Init initializes postgres session provider. +// connStr: user=a password=b host=localhost port=5432 dbname=c sslmode=disable +func (p *PostgresProvider) Init(maxlifetime int64, connStr string) (err error) { + p.maxlifetime = maxlifetime + + p.c, err = sql.Open("postgres", connStr) + if err != nil { + return err + } + return p.c.Ping() +} + +// Read returns raw session store by session ID. +func (p *PostgresProvider) Read(sid string) (session.RawStore, error) { + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE key=$1", sid).Scan(&data) + if err == sql.ErrNoRows { + _, err = p.c.Exec("INSERT INTO session(key,data,expiry) VALUES($1,$2,$3)", + sid, "", time.Now().Unix()) + } + if err != nil { + return nil, err + } + + var kv map[interface{}]interface{} + if len(data) == 0 { + kv = make(map[interface{}]interface{}) + } else { + kv, err = session.DecodeGob(data) + if err != nil { + return nil, err + } + } + + return NewPostgresStore(p.c, sid, kv), nil +} + +// Exist returns true if session with given ID exists. +func (p *PostgresProvider) Exist(sid string) bool { + var data []byte + err := p.c.QueryRow("SELECT data FROM session WHERE key=$1", sid).Scan(&data) + if err != nil && err != sql.ErrNoRows { + panic("session/postgres: error checking existence: " + err.Error()) + } + return err != sql.ErrNoRows +} + +// Destory deletes a session by session ID. +func (p *PostgresProvider) Destory(sid string) error { + _, err := p.c.Exec("DELETE FROM session WHERE key=$1", sid) + return err +} + +// Regenerate regenerates a session store from old session ID to new one. +func (p *PostgresProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) + } + + if !p.Exist(oldsid) { + if _, err = p.c.Exec("INSERT INTO session(key,data,expiry) VALUES($1,$2,$3)", + oldsid, "", time.Now().Unix()); err != nil { + return nil, err + } + } + + if _, err = p.c.Exec("UPDATE session SET key=$1 WHERE key=$2", sid, oldsid); err != nil { + return nil, err + } + + return p.Read(sid) +} + +// Count counts and returns number of sessions. +func (p *PostgresProvider) Count() (total int) { + if err := p.c.QueryRow("SELECT COUNT(*) AS NUM FROM session").Scan(&total); err != nil { + panic("session/postgres: error counting records: " + err.Error()) + } + return total +} + +// GC calls GC to clean expired sessions. +func (p *PostgresProvider) GC() { + if _, err := p.c.Exec("DELETE FROM session WHERE EXTRACT(EPOCH FROM NOW()) - expiry > $1", p.maxlifetime); err != nil { + log.Printf("session/postgres: error garbage collecting: %v", err) + } +} + +func init() { + session.Register("postgres", &PostgresProvider{}) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.goconvey b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.goconvey new file mode 100644 index 00000000000..8485e986e45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres.goconvey @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres_test.go new file mode 100644 index 00000000000..ea212c729f6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgres_test.go @@ -0,0 +1,138 @@ +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/Unknwon/macaron" + . "github.com/smartystreets/goconvey/convey" + + "github.com/macaron-contrib/session" +) + +func Test_PostgresProvider(t *testing.T) { + Convey("Test postgres session provider", t, func() { + opt := session.Options{ + Provider: "postgres", + ProviderConfig: "user=jiahuachen dbname=macaron port=5432 sslmode=disable", + } + + Convey("Basic operation", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + sess.Set("uname", "unknwon") + }) + m.Get("/reg", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := raw.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + }) + m.Get("/get", func(ctx *macaron.Context, sess session.Store) { + sid := sess.ID() + So(sid, ShouldNotBeEmpty) + + raw, err := sess.Read(sid) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + So(raw.Release(), ShouldBeNil) + + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Delete("uname"), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + + cookie := resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/reg", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + cookie = resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/get", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + }) + + Convey("Regenrate empty session", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", "MacaronSession=ad2c7e3cbecfcf48; Path=/;") + m.ServeHTTP(resp, req) + }) + + Convey("GC session", func() { + m := macaron.New() + opt2 := opt + opt2.Gclifetime = 1 + m.Use(session.Sessioner(opt2)) + + m.Get("/", func(sess session.Store) { + sess.Set("uname", "unknwon") + So(sess.ID(), ShouldNotBeEmpty) + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Flush(), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + time.Sleep(2 * time.Second) + sess.GC() + So(sess.Count(), ShouldEqual, 0) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgresql.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgresql.go deleted file mode 100644 index bfe87c69ee0..00000000000 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/postgres/postgresql.go +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2013 Beego Authors -// Copyright 2014 Unknwon -// -// Licensed under the Apache License, Version 2.0 (the "License"): you may -// not use this file except in compliance with the License. You may obtain -// a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -// License for the specific language governing permissions and limitations -// under the License. - -package session - -import ( - "database/sql" - "sync" - "time" - - _ "github.com/lib/pq" - - "github.com/macaron-contrib/session" -) - -// PostgresqlSessionStore represents a postgresql session store implementation. -type PostgresqlSessionStore struct { - c *sql.DB - sid string - lock sync.RWMutex - data map[interface{}]interface{} -} - -// Set sets value to given key in session. -func (s *PostgresqlSessionStore) Set(key, value interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data[key] = value - return nil -} - -// Get gets value by given key in session. -func (s *PostgresqlSessionStore) Get(key interface{}) interface{} { - s.lock.RLock() - defer s.lock.RUnlock() - - return s.data[key] -} - -// Delete delete a key from session. -func (s *PostgresqlSessionStore) Delete(key interface{}) error { - s.lock.Lock() - defer s.lock.Unlock() - - delete(s.data, key) - return nil -} - -// ID returns current session ID. -func (s *PostgresqlSessionStore) ID() string { - return s.sid -} - -// save postgresql session values to database. -// must call this method to save values to database. -func (s *PostgresqlSessionStore) Release() error { - defer s.c.Close() - - data, err := session.EncodeGob(s.data) - if err != nil { - return err - } - - _, err = s.c.Exec("UPDATE session set session_data=$1, session_expiry=$2 where session_key=$3", - data, time.Now().Format(time.RFC3339), s.sid) - return err -} - -// Flush deletes all session data. -func (s *PostgresqlSessionStore) Flush() error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = make(map[interface{}]interface{}) - return nil -} - -// PostgresqlProvider represents a postgresql session provider implementation. -type PostgresqlProvider struct { - maxlifetime int64 - connStr string -} - -func (p *PostgresqlProvider) connectInit() *sql.DB { - db, e := sql.Open("postgres", p.connStr) - if e != nil { - return nil - } - return db -} - -// Init initializes memory session provider. -func (p *PostgresqlProvider) Init(maxlifetime int64, connStr string) error { - p.maxlifetime = maxlifetime - p.connStr = connStr - return nil -} - -// Read returns raw session store by session ID. -func (p *PostgresqlProvider) Read(sid string) (session.RawStore, error) { - c := p.connectInit() - row := c.QueryRow("select session_data from session where session_key=$1", sid) - var sessiondata []byte - err := row.Scan(&sessiondata) - if err == sql.ErrNoRows { - _, err = c.Exec("insert into session(session_key,session_data,session_expiry) values($1,$2,$3)", - sid, "", time.Now().Format(time.RFC3339)) - - if err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - - var kv map[interface{}]interface{} - if len(sessiondata) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob(sessiondata) - if err != nil { - return nil, err - } - } - rs := &PostgresqlSessionStore{c: c, sid: sid, data: kv} - return rs, nil -} - -// Exist returns true if session with given ID exists. -func (p *PostgresqlProvider) Exist(sid string) bool { - c := p.connectInit() - defer c.Close() - row := c.QueryRow("select session_data from session where session_key=$1", sid) - var sessiondata []byte - err := row.Scan(&sessiondata) - - if err == sql.ErrNoRows { - return false - } else { - return true - } -} - -// Destory deletes a session by session ID. -func (p *PostgresqlProvider) Destory(sid string) (err error) { - c := p.connectInit() - if _, err = c.Exec("DELETE FROM session where session_key=$1", sid); err != nil { - return err - } - return c.Close() -} - -// Regenerate regenerates a session store from old session ID to new one. -func (p *PostgresqlProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { - c := p.connectInit() - row := c.QueryRow("select session_data from session where session_key=$1", oldsid) - var sessiondata []byte - err := row.Scan(&sessiondata) - if err == sql.ErrNoRows { - c.Exec("insert into session(session_key,session_data,session_expiry) values($1,$2,$3)", - oldsid, "", time.Now().Format(time.RFC3339)) - } - c.Exec("update session set session_key=$1 where session_key=$2", sid, oldsid) - var kv map[interface{}]interface{} - if len(sessiondata) == 0 { - kv = make(map[interface{}]interface{}) - } else { - kv, err = session.DecodeGob(sessiondata) - if err != nil { - return nil, err - } - } - rs := &PostgresqlSessionStore{c: c, sid: sid, data: kv} - return rs, nil -} - -// Count counts and returns number of sessions. -func (p *PostgresqlProvider) Count() int { - c := p.connectInit() - defer c.Close() - var total int - err := c.QueryRow("SELECT count(*) as num from session").Scan(&total) - if err != nil { - return 0 - } - return total -} - -// GC calls GC to clean expired sessions. -func (mp *PostgresqlProvider) GC() { - c := mp.connectInit() - c.Exec("DELETE from session where EXTRACT(EPOCH FROM (current_timestamp - session_expiry)) > $1", mp.maxlifetime) - c.Close() -} - -func init() { - session.Register("postgresql", &PostgresqlProvider{}) -} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.go index 211866317f3..6d6a2c464c8 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.go @@ -16,31 +16,39 @@ package session import ( - "strconv" + "fmt" "strings" "sync" + "time" - "github.com/beego/redigo/redis" + "github.com/Unknwon/com" + "gopkg.in/ini.v1" + "gopkg.in/redis.v2" "github.com/macaron-contrib/session" ) -// redis max pool size -var MAX_POOL_SIZE = 100 +// RedisStore represents a redis session store implementation. +type RedisStore struct { + c *redis.Client + sid string + duration time.Duration + lock sync.RWMutex + data map[interface{}]interface{} +} -var redisPool chan redis.Conn - -// RedisSessionStore represents a redis session store implementation. -type RedisSessionStore struct { - p *redis.Pool - sid string - lock sync.RWMutex - data map[interface{}]interface{} - maxlifetime int64 +// NewRedisStore creates and returns a redis session store. +func NewRedisStore(c *redis.Client, sid string, dur time.Duration, kv map[interface{}]interface{}) *RedisStore { + return &RedisStore{ + c: c, + sid: sid, + duration: dur, + data: kv, + } } // Set sets value to given key in session. -func (s *RedisSessionStore) Set(key, val interface{}) error { +func (s *RedisStore) Set(key, val interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -49,7 +57,7 @@ func (s *RedisSessionStore) Set(key, val interface{}) error { } // Get gets value by given key in session. -func (s *RedisSessionStore) Get(key interface{}) interface{} { +func (s *RedisStore) Get(key interface{}) interface{} { s.lock.RLock() defer s.lock.RUnlock() @@ -57,7 +65,7 @@ func (s *RedisSessionStore) Get(key interface{}) interface{} { } // Delete delete a key from session. -func (s *RedisSessionStore) Delete(key interface{}) error { +func (s *RedisStore) Delete(key interface{}) error { s.lock.Lock() defer s.lock.Unlock() @@ -66,26 +74,22 @@ func (s *RedisSessionStore) Delete(key interface{}) error { } // ID returns current session ID. -func (s *RedisSessionStore) ID() string { +func (s *RedisStore) ID() string { return s.sid } // Release releases resource and save data to provider. -func (s *RedisSessionStore) Release() error { - c := s.p.Get() - defer c.Close() - +func (s *RedisStore) Release() error { data, err := session.EncodeGob(s.data) if err != nil { return err } - _, err = c.Do("SETEX", s.sid, s.maxlifetime, string(data)) - return err + return s.c.SetEx(s.sid, s.duration, string(data)).Err() } // Flush deletes all session data. -func (s *RedisSessionStore) Flush() error { +func (s *RedisStore) Flush() error { s.lock.Lock() defer s.lock.Unlock() @@ -95,59 +99,65 @@ func (s *RedisSessionStore) Flush() error { // RedisProvider represents a redis session provider implementation. type RedisProvider struct { - maxlifetime int64 - connAddr string - poolsize int - password string - poollist *redis.Pool + c *redis.Client + duration time.Duration } -// Init initializes memory session provider. -// connStr: ,, -// e.g. 127.0.0.1:6379,100,macaron -func (p *RedisProvider) Init(maxlifetime int64, connStr string) error { - p.maxlifetime = maxlifetime - configs := strings.Split(connStr, ",") - if len(configs) > 0 { - p.connAddr = configs[0] +// Init initializes redis session provider. +// configs: network=tcp,addr=:6379,password=macaron,db=0,pool_size=100,idle_timeout=180 +func (p *RedisProvider) Init(maxlifetime int64, configs string) (err error) { + p.duration, err = time.ParseDuration(fmt.Sprintf("%ds", maxlifetime)) + if err != nil { + return err } - if len(configs) > 1 { - poolsize, err := strconv.Atoi(configs[1]) - if err != nil || poolsize <= 0 { - p.poolsize = MAX_POOL_SIZE - } else { - p.poolsize = poolsize - } - } else { - p.poolsize = MAX_POOL_SIZE - } - if len(configs) > 2 { - p.password = configs[2] - } - p.poollist = redis.NewPool(func() (redis.Conn, error) { - c, err := redis.Dial("tcp", p.connAddr) - if err != nil { - return nil, err - } - if p.password != "" { - if _, err := c.Do("AUTH", p.password); err != nil { - c.Close() - return nil, err - } - } - return c, err - }, p.poolsize) - return p.poollist.Get().Err() + cfg, err := ini.Load([]byte(strings.Replace(configs, ",", "\n", -1))) + if err != nil { + return err + } + + opt := &redis.Options{ + Network: "tcp", + } + for k, v := range cfg.Section("").KeysHash() { + switch k { + case "network": + opt.Network = v + case "addr": + opt.Addr = v + case "password": + opt.Password = v + case "db": + opt.DB = com.StrTo(v).MustInt64() + case "pool_size": + opt.PoolSize = com.StrTo(v).MustInt() + case "idle_timeout": + opt.IdleTimeout, err = time.ParseDuration(v + "s") + if err != nil { + return fmt.Errorf("error parsing idle timeout: %v", err) + } + default: + return fmt.Errorf("session/redis: unsupported option '%s'", k) + } + } + + p.c = redis.NewClient(opt) + return p.c.Ping().Err() } // Read returns raw session store by session ID. func (p *RedisProvider) Read(sid string) (session.RawStore, error) { - c := p.poollist.Get() - defer c.Close() + if !p.Exist(sid) { + if err := p.c.Set(sid, "").Err(); err != nil { + return nil, err + } + } - kvs, err := redis.String(c.Do("GET", sid)) var kv map[interface{}]interface{} + kvs, err := p.c.Get(sid).Result() + if err != nil { + return nil, err + } if len(kvs) == 0 { kv = make(map[interface{}]interface{}) } else { @@ -157,48 +167,41 @@ func (p *RedisProvider) Read(sid string) (session.RawStore, error) { } } - rs := &RedisSessionStore{p: p.poollist, sid: sid, data: kv, maxlifetime: p.maxlifetime} - return rs, nil + return NewRedisStore(p.c, sid, p.duration, kv), nil } // Exist returns true if session with given ID exists. func (p *RedisProvider) Exist(sid string) bool { - c := p.poollist.Get() - defer c.Close() - - if existed, err := redis.Int(c.Do("EXISTS", sid)); err != nil || existed == 0 { - return false - } else { - return true - } + has, err := p.c.Exists(sid).Result() + return err == nil && has } // Destory deletes a session by session ID. func (p *RedisProvider) Destory(sid string) error { - c := p.poollist.Get() - defer c.Close() - - _, err := c.Do("DEL", sid) - return err + return p.c.Del(sid).Err() } // Regenerate regenerates a session store from old session ID to new one. -func (p *RedisProvider) Regenerate(oldsid, sid string) (session.RawStore, error) { - c := p.poollist.Get() - defer c.Close() - - if existed, _ := redis.Int(c.Do("EXISTS", oldsid)); existed == 0 { - // oldsid doesn't exists, set the new sid directly - // ignore error here, since if it return error - // the existed value will be 0 - c.Do("SET", sid, "", "EX", p.maxlifetime) - } else { - c.Do("RENAME", oldsid, sid) - c.Do("EXPIRE", sid, p.maxlifetime) +func (p *RedisProvider) Regenerate(oldsid, sid string) (_ session.RawStore, err error) { + if p.Exist(sid) { + return nil, fmt.Errorf("new sid '%s' already exists", sid) + } else if !p.Exist(oldsid) { + // Make a fake old session. + if err = p.c.SetEx(oldsid, p.duration, "").Err(); err != nil { + return nil, err + } + } + + if err = p.c.Rename(oldsid, sid).Err(); err != nil { + return nil, err } - kvs, err := redis.String(c.Do("GET", sid)) var kv map[interface{}]interface{} + kvs, err := p.c.Get(sid).Result() + if err != nil { + return nil, err + } + if len(kvs) == 0 { kv = make(map[interface{}]interface{}) } else { @@ -208,14 +211,12 @@ func (p *RedisProvider) Regenerate(oldsid, sid string) (session.RawStore, error) } } - rs := &RedisSessionStore{p: p.poollist, sid: sid, data: kv, maxlifetime: p.maxlifetime} - return rs, nil + return NewRedisStore(p.c, sid, p.duration, kv), nil } // Count counts and returns number of sessions. func (p *RedisProvider) Count() int { - // FIXME - return 0 + return int(p.c.DbSize().Val()) } // GC calls GC to clean expired sessions. diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.goconvey b/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.goconvey new file mode 100644 index 00000000000..8485e986e45 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis.goconvey @@ -0,0 +1 @@ +ignore \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis_test.go new file mode 100644 index 00000000000..9fd8e6518f8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/redis/redis_test.go @@ -0,0 +1,107 @@ +// Copyright 2014 Unknwon +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package session + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/Unknwon/macaron" + . "github.com/smartystreets/goconvey/convey" + + "github.com/macaron-contrib/session" +) + +func Test_RedisProvider(t *testing.T) { + Convey("Test redis session provider", t, func() { + opt := session.Options{ + Provider: "redis", + ProviderConfig: "addr=:6379", + } + + Convey("Basic operation", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + sess.Set("uname", "unknwon") + }) + m.Get("/reg", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := raw.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + }) + m.Get("/get", func(ctx *macaron.Context, sess session.Store) { + sid := sess.ID() + So(sid, ShouldNotBeEmpty) + + raw, err := sess.Read(sid) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + + uname := sess.Get("uname") + So(uname, ShouldNotBeNil) + So(uname, ShouldEqual, "unknwon") + + So(sess.Delete("uname"), ShouldBeNil) + So(sess.Get("uname"), ShouldBeNil) + + So(sess.Destory(ctx), ShouldBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + + cookie := resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/reg", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + + cookie = resp.Header().Get("Set-Cookie") + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/get", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", cookie) + m.ServeHTTP(resp, req) + }) + + Convey("Regenrate empty session", func() { + m := macaron.New() + m.Use(session.Sessioner(opt)) + m.Get("/", func(ctx *macaron.Context, sess session.Store) { + raw, err := sess.RegenerateId(ctx) + So(err, ShouldBeNil) + So(raw, ShouldNotBeNil) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/", nil) + So(err, ShouldBeNil) + req.Header.Set("Cookie", "MacaronSession=ad2c7e3cbecfcf486; Path=/;") + m.ServeHTTP(resp, req) + }) + }) +} diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/session.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/session.go index 204c65d7668..9cc1d528749 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/session.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/session.go @@ -13,7 +13,7 @@ // License for the specific language governing permissions and limitations // under the License. -// Package session a middleware that provides the session manager of Macaron. +// Package session a middleware that provides the session management of Macaron. package session // NOTE: last sync 000033e on Nov 4, 2014. @@ -28,7 +28,7 @@ import ( "github.com/Unknwon/macaron" ) -const _VERSION = "0.1.1" +const _VERSION = "0.1.6" func Version() string { return _VERSION @@ -37,11 +37,11 @@ func Version() string { // RawStore is the interface that operates the session data. type RawStore interface { // Set sets value to given key in session. - Set(key, value interface{}) error + Set(interface{}, interface{}) error // Get gets value by given key in session. - Get(key interface{}) interface{} - // Delete delete a key from session. - Delete(key interface{}) error + Get(interface{}) interface{} + // Delete deletes a key from session. + Delete(interface{}) error // ID returns current session ID. ID() string // Release releases session resource and save data to provider. @@ -54,7 +54,7 @@ type RawStore interface { type Store interface { RawStore // Read returns raw session store by session ID. - Read(sid string) (RawStore, error) + Read(string) (RawStore, error) // Destory deletes a session. Destory(*macaron.Context) error // RegenerateId regenerates a session store from old session ID to new one. @@ -111,7 +111,7 @@ func prepareOptions(options []Options) Options { if len(opt.Provider) == 0 { opt.Provider = sec.Key("PROVIDER").MustString("memory") } - if len(opt.ProviderConfig) == 0 && opt.Provider == "file" { + if len(opt.ProviderConfig) == 0 { opt.ProviderConfig = sec.Key("PROVIDER_CONFIG").MustString("data/sessions") } if len(opt.CookieName) == 0 { @@ -155,7 +155,7 @@ func Sessioner(options ...Options) macaron.Handler { return func(ctx *macaron.Context) { sess, err := manager.Start(ctx) if err != nil { - panic("session: " + err.Error()) + panic("session(start): " + err.Error()) } // Get flash. @@ -187,8 +187,8 @@ func Sessioner(options ...Options) macaron.Handler { ctx.Next() - if sess.Release() != nil { - panic("session: " + err.Error()) + if err = sess.Release(); err != nil { + panic("session(release): " + err.Error()) } } } @@ -242,17 +242,14 @@ type Manager struct { func NewManager(name string, opt Options) (*Manager, error) { p, ok := providers[name] if !ok { - return nil, fmt.Errorf("session: unknown provider ‘%q’(forgotten import?)", name) + return nil, fmt.Errorf("session: unknown provider '%s'(forgotten import?)", name) } - if err := p.Init(opt.Maxlifetime, opt.ProviderConfig); err != nil { - return nil, err - } - return &Manager{p, opt}, nil + return &Manager{p, opt}, p.Init(opt.Maxlifetime, opt.ProviderConfig) } // sessionId generates a new session ID with rand string, unix nano time, remote addr by hash function. func (m *Manager) sessionId() string { - return hex.EncodeToString(generateRandomKey(m.opt.IDLength)) + return hex.EncodeToString(generateRandomKey(m.opt.IDLength / 2)) } // Start starts a session by generating new one @@ -315,16 +312,9 @@ func (m *Manager) Destory(ctx *macaron.Context) error { func (m *Manager) RegenerateId(ctx *macaron.Context) (sess RawStore, err error) { sid := m.sessionId() oldsid := ctx.GetCookie(m.opt.CookieName) - if len(oldsid) == 0 { - sess, err = m.provider.Read(oldsid) - if err != nil { - return nil, err - } - } else { - sess, err = m.provider.Regenerate(oldsid, sid) - if err != nil { - return nil, err - } + sess, err = m.provider.Regenerate(oldsid, sid) + if err != nil { + return nil, err } ck := &http.Cookie{ Name: m.opt.CookieName, diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/session_test.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/session_test.go index 327c1d0c6a5..82efc277c61 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/session_test.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/session_test.go @@ -42,7 +42,7 @@ func Test_Sessioner(t *testing.T) { m.ServeHTTP(resp, req) }) - Convey("Register invalid provider that", t, func() { + Convey("Register invalid provider", t, func() { Convey("Provider not exists", func() { defer func() { So(recover(), ShouldNotBeNil) diff --git a/Godeps/_workspace/src/github.com/macaron-contrib/session/utils.go b/Godeps/_workspace/src/github.com/macaron-contrib/session/utils.go index a165de83070..6c9ea495fe5 100644 --- a/Godeps/_workspace/src/github.com/macaron-contrib/session/utils.go +++ b/Godeps/_workspace/src/github.com/macaron-contrib/session/utils.go @@ -24,39 +24,19 @@ import ( "github.com/Unknwon/com" ) -func init() { - gob.Register([]interface{}{}) - gob.Register(map[int]interface{}{}) - gob.Register(map[string]interface{}{}) - gob.Register(map[interface{}]interface{}{}) - gob.Register(map[string]string{}) - gob.Register(map[int]string{}) - gob.Register(map[int]int{}) - gob.Register(map[int]int64{}) -} - func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) { for _, v := range obj { gob.Register(v) } buf := bytes.NewBuffer(nil) - enc := gob.NewEncoder(buf) - err := enc.Encode(obj) - if err != nil { - return []byte(""), err - } - return buf.Bytes(), nil + err := gob.NewEncoder(buf).Encode(obj) + return buf.Bytes(), err } -func DecodeGob(encoded []byte) (map[interface{}]interface{}, error) { +func DecodeGob(encoded []byte) (out map[interface{}]interface{}, err error) { buf := bytes.NewBuffer(encoded) - dec := gob.NewDecoder(buf) - var out map[interface{}]interface{} - err := dec.Decode(&out) - if err != nil { - return nil, err - } - return out, nil + err = gob.NewDecoder(buf).Decode(&out) + return out, err } // generateRandomKey creates a random key with the given strength.