Updated readme

This commit is contained in:
Torkel Ödegaard
2014-12-30 10:37:00 +01:00
parent aafe2c5b98
commit ef90f1dca6
7 changed files with 803 additions and 45 deletions

View File

@@ -0,0 +1,259 @@
// 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 (
"fmt"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"sync"
"time"
"github.com/Unknwon/com"
)
// FileSessionStore represents a file session store implementation.
type FileSessionStore 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{
p: p,
sid: sid,
data: kv,
}
}
// Set sets value to given key in session.
func (s *FileSessionStore) 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 *FileSessionStore) Get(key interface{}) interface{} {
s.lock.RLock()
defer s.lock.RUnlock()
return s.data[key]
}
// Delete delete a key from session.
func (s *FileSessionStore) Delete(key interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data, key)
return nil
}
// ID returns current session ID.
func (s *FileSessionStore) ID() string {
return s.sid
}
// Release releases resource and save data to provider.
func (s *FileSessionStore) Release() error {
data, err := EncodeGob(s.data)
if err != nil {
return err
}
return ioutil.WriteFile(s.p.filepath(s.sid), data, os.ModePerm)
}
// Flush deletes all session data.
func (s *FileSessionStore) Flush() error {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[interface{}]interface{})
return nil
}
// FileProvider represents a file session provider implementation.
type FileProvider struct {
lock sync.RWMutex
maxlifetime int64
rootPath string
}
// Init initializes file session provider with given root path.
func (p *FileProvider) Init(maxlifetime int64, rootPath string) error {
p.maxlifetime = maxlifetime
p.rootPath = rootPath
return nil
}
func (p *FileProvider) filepath(sid string) string {
return path.Join(p.rootPath, string(sid[0]), string(sid[1]), sid)
}
// 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
}
var f *os.File
if com.IsFile(filename) {
f, err = os.OpenFile(filename, os.O_RDWR, os.ModePerm)
} else {
f, err = os.Create(filename)
}
if err != nil {
return nil, err
}
defer f.Close()
if err = os.Chtimes(filename, time.Now(), time.Now()); err != nil {
return nil, err
}
var kv map[interface{}]interface{}
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
if len(data) == 0 {
kv = make(map[interface{}]interface{})
} else {
kv, err = DecodeGob(data)
if err != nil {
return nil, err
}
}
return NewFileSessionStore(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))
}
func (p *FileProvider) regenerate(oldsid, sid string) (err error) {
filename := p.filepath(sid)
if com.IsExist(filename) {
return fmt.Errorf("new sid '%s' already exists", sid)
}
oldname := p.filepath(oldsid)
if !com.IsFile(oldname) {
data, err := EncodeGob(make(map[interface{}]interface{}))
if err != nil {
return err
}
if err = os.MkdirAll(path.Dir(oldname), os.ModePerm); err != nil {
return err
}
if err = ioutil.WriteFile(oldname, data, os.ModePerm); err != nil {
return err
}
}
if err = os.MkdirAll(path.Dir(filename), os.ModePerm); err != nil {
return err
}
if err = os.Rename(oldname, filename); err != nil {
return err
}
return nil
}
// 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)
}
// Count counts and returns number of sessions.
func (p *FileProvider) Count() int {
count := 0
if err := filepath.Walk(p.rootPath, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.IsDir() {
count++
}
return nil
}); err != nil {
log.Printf("error counting session files: %v", err)
return 0
}
return count
}
// GC calls GC to clean expired sessions.
func (p *FileProvider) GC() {
if !com.IsExist(p.rootPath) {
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
}
if !fi.IsDir() &&
(fi.ModTime().Unix()+p.maxlifetime) < time.Now().Unix() {
return os.Remove(path)
}
return nil
}); err != nil {
log.Printf("error garbage collecting session files: %v", err)
}
}
func init() {
Register("file", &FileProvider{})
}

View File

@@ -0,0 +1,34 @@
// 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 (
"os"
"path"
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_FileProvider(t *testing.T) {
Convey("Test file session provider", t, func() {
dir := path.Join(os.TempDir(), "data/sessions")
os.RemoveAll(dir)
testProvider(Options{
Provider: "file",
ProviderConfig: dir,
})
})
}

View File

@@ -0,0 +1,212 @@
// 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 (
"container/list"
"fmt"
"sync"
"time"
)
// MemSessionStore represents a in-memory session store implementation.
type MemSessionStore 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{
sid: sid,
data: make(map[interface{}]interface{}),
lastAccess: time.Now(),
}
}
// Set sets value to given key in session.
func (s *MemSessionStore) 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 *MemSessionStore) 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 {
s.lock.Lock()
defer s.lock.Unlock()
delete(s.data, key)
return nil
}
// ID returns current session ID.
func (s *MemSessionStore) ID() string {
return s.sid
}
// Release releases resource and save data to provider.
func (_ *MemSessionStore) Release() error {
return nil
}
// Flush deletes all session data.
func (s *MemSessionStore) Flush() error {
s.lock.Lock()
defer s.lock.Unlock()
s.data = make(map[interface{}]interface{})
return nil
}
// MemProvider represents a in-memory session provider implementation.
type MemProvider struct {
lock sync.RWMutex
maxLifetime int64
data map[string]*list.Element
// A priority list whose lastAccess newer gets higer priority.
list *list.List
}
// Init initializes memory session provider.
func (p *MemProvider) Init(maxLifetime int64, _ string) error {
p.maxLifetime = maxLifetime
return nil
}
// update expands time of session store by given ID.
func (p *MemProvider) update(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
if e, ok := p.data[sid]; ok {
e.Value.(*MemSessionStore).lastAccess = time.Now()
p.list.MoveToFront(e)
return nil
}
return nil
}
// Read returns raw session store by session ID.
func (p *MemProvider) Read(sid string) (_ RawStore, err error) {
p.lock.RLock()
e, ok := p.data[sid]
p.lock.RUnlock()
if ok {
if err = p.update(sid); err != nil {
return nil, err
}
return e.Value.(*MemSessionStore), nil
}
// Create a new session.
p.lock.Lock()
defer p.lock.Unlock()
s := NewMemSessionStore(sid)
p.data[sid] = p.list.PushBack(s)
return s, nil
}
// Exist returns true if session with given ID exists.
func (p *MemProvider) Exist(sid string) bool {
p.lock.RLock()
defer p.lock.RUnlock()
_, ok := p.data[sid]
return ok
}
// Destory deletes a session by session ID.
func (p *MemProvider) Destory(sid string) error {
p.lock.Lock()
defer p.lock.Unlock()
e, ok := p.data[sid]
if !ok {
return nil
}
p.list.Remove(e)
delete(p.data, sid)
return nil
}
// Regenerate regenerates a session store from old session ID to new one.
func (p *MemProvider) Regenerate(oldsid, sid string) (RawStore, error) {
if p.Exist(sid) {
return nil, fmt.Errorf("new sid '%s' already exists", sid)
}
s, err := p.Read(oldsid)
if err != nil {
return nil, err
}
if err = p.Destory(oldsid); err != nil {
return nil, err
}
s.(*MemSessionStore).sid = sid
p.data[sid] = p.list.PushBack(s)
return s, nil
}
// Count counts and returns number of sessions.
func (p *MemProvider) Count() int {
return p.list.Len()
}
// GC calls GC to clean expired sessions.
func (p *MemProvider) GC() {
p.lock.RLock()
for {
// No session in the list.
e := p.list.Back()
if e == nil {
break
}
if (e.Value.(*MemSessionStore).lastAccess.Unix() + p.maxLifetime) < time.Now().Unix() {
p.lock.RUnlock()
p.lock.Lock()
p.list.Remove(e)
delete(p.data, e.Value.(*MemSessionStore).sid)
p.lock.Unlock()
p.lock.RLock()
} else {
break
}
}
p.lock.RUnlock()
}
func init() {
Register("memory", &MemProvider{list: list.New(), data: make(map[string]*list.Element)})
}

View File

@@ -0,0 +1,27 @@
// 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 (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_MemProvider(t *testing.T) {
Convey("Test memory session provider", t, func() {
testProvider(Options{})
})
}

View File

@@ -0,0 +1,200 @@
// 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"
)
func Test_Version(t *testing.T) {
Convey("Check package version", t, func() {
So(Version(), ShouldEqual, _VERSION)
})
}
func Test_Sessioner(t *testing.T) {
Convey("Use session middleware", t, func() {
m := macaron.New()
m.Use(Sessioner())
m.Get("/", func() {})
resp := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/", nil)
So(err, ShouldBeNil)
m.ServeHTTP(resp, req)
})
Convey("Register invalid provider that", t, func() {
Convey("Provider not exists", func() {
defer func() {
So(recover(), ShouldNotBeNil)
}()
m := macaron.New()
m.Use(Sessioner(Options{
Provider: "fake",
}))
})
Convey("Provider value is nil", func() {
defer func() {
So(recover(), ShouldNotBeNil)
}()
Register("fake", nil)
})
Convey("Register twice", func() {
defer func() {
So(recover(), ShouldNotBeNil)
}()
Register("memory", &MemProvider{})
})
})
}
func testProvider(opt Options) {
Convey("Basic operation", func() {
m := macaron.New()
m.Use(Sessioner(opt))
m.Get("/", func(ctx *macaron.Context, sess Store) {
sess.Set("uname", "unknwon")
})
m.Get("/reg", func(ctx *macaron.Context, sess 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 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(Sessioner(opt))
m.Get("/", func(ctx *macaron.Context, sess 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)
})
Convey("GC session", func() {
m := macaron.New()
opt2 := opt
opt2.Gclifetime = 1
m.Use(Sessioner(opt2))
m.Get("/", func(sess 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)
})
}
func Test_Flash(t *testing.T) {
Convey("Test flash", t, func() {
m := macaron.New()
m.Use(Sessioner())
m.Get("/set", func(f *Flash) string {
f.Success("success")
f.Error("error")
f.Warning("warning")
f.Info("info")
return ""
})
m.Get("/get", func() {})
resp := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/set", nil)
So(err, ShouldBeNil)
m.ServeHTTP(resp, req)
resp = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/get", nil)
So(err, ShouldBeNil)
req.Header.Set("Cookie", "macaron_flash=error%3Derror%26info%3Dinfo%26success%3Dsuccess%26warning%3Dwarning; Path=/")
m.ServeHTTP(resp, req)
})
}

View File

@@ -0,0 +1,69 @@
// 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 (
"bytes"
"crypto/rand"
"encoding/gob"
"io"
"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
}
func DecodeGob(encoded []byte) (map[interface{}]interface{}, 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
}
// generateRandomKey creates a random key with the given strength.
func generateRandomKey(strength int) []byte {
k := make([]byte, strength)
if n, err := io.ReadFull(rand.Reader, k); n != strength || err != nil {
return com.RandomCreateBytes(strength)
}
return k
}