From ef9866c6f5626df3f68fdc08439a7a306a8f3219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Lindstr=C3=B6m?= Date: Tue, 7 Jan 2014 17:15:27 +0000 Subject: [PATCH] initial commit --- Makefile | 7 ++ README.md | 55 +++++++++++++++ pgstore.go | 193 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 pgstore.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4a848d1 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +all: get-deps + +get-deps: + @go get -d -v ./... + +format: + @go fmt ./... diff --git a/README.md b/README.md new file mode 100644 index 0000000..1452ff8 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# pgstore + +A session store backend for [gorilla/sessions](http://www.gorillatoolkit.org/pkg/sessions) - [src](https://github.com/gorilla/sessions). + +## Installation + + make get-deps + +## Documentation + +Available on [godoc.org](http://www.godoc.org/github.com/boj/redistore). + +See http://www.gorillatoolkit.org/pkg/sessions for full documentation on underlying interface. + +### Example + + // Fetch new store. + store := NewPGStore("postgres://user:password@127.0.0.1:5432/database?sslmode=verify-full", []byte("secret-key")) + defer store.Close() + + // Get a session. + session, err = store.Get(req, "session-key") + if err != nil { + log.Error(err.Error()) + } + + // Add a value. + session.Values["foo"] = "bar" + + // Save. + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + + // Delete session. + session.Options.MaxAge = -1 + if err = sessions.Save(req, rsp); err != nil { + t.Fatalf("Error saving session: %v", err) + } + +## Thanks + +I've stolen, borrowed and gotten inspiration from the other backends available: + +* [redistore](https://github.com/boj/redistore) +* [mysqlstore](https://github.com/srinathgs/mysqlstore) +* [babou dbstore](https://github.com/drbawb/babou/blob/master/lib/session/dbstore.go) + +Thank you all for sharing your code! + +What makes this backend different is that it's for Postgresql and uses the fine +datamapper [Gorp](https://github.com/coopernurse/gorp). +Make sure you use a somewhat new codebase of Gorp as it now defaults to text for +strings when it used to default to varchar 255. Varchar 255 is unfortunately too +small. diff --git a/pgstore.go b/pgstore.go new file mode 100644 index 0000000..3d0c4f0 --- /dev/null +++ b/pgstore.go @@ -0,0 +1,193 @@ +package pgstore + +import ( + "database/sql" + "encoding/base32" + "github.com/coopernurse/gorp" + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" + _ "github.com/lib/pq" + "net/http" + "strings" + "time" +) + +type PGStore struct { + Codecs []securecookie.Codec + Options *sessions.Options + path string + DbMap *gorp.DbMap +} + +// Session type +type Session struct { + Id int64 `db: "id"` + Key string `db: "key"` + Data string `db: "data"` + CreatedOn time.Time `db: "created_on"` + ModifiedOn time.Time `db: "modified_on"` + ExpiresOn time.Time `db: "expires_on"` +} + +func NewPGStore(dbUrl string, keyPairs ...[]byte) *PGStore { + db, err := sql.Open("postgres", dbUrl) + dbmap := &gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}} + + dbStore := &PGStore{ + Codecs: securecookie.CodecsFromPairs(keyPairs...), + Options: &sessions.Options{ + Path: "/", + MaxAge: 86400 * 30, + }, + DbMap: dbmap, + } + + if err != nil { + // Ignore and return nil + //fmt.Printf("SQL connection error: %s\n", err) + return nil + } + + // Create table if it doesn't exist + dbmap.AddTableWithName(Session{}, "http_sessions").SetKeys(true, "Id") + err = dbmap.CreateTablesIfNotExists() + + if err != nil { + // Ignore and return nil + //fmt.Printf("Failed to create table http_sessions. Error: %s\n", err) + return nil + } + + return dbStore +} + +func (db *PGStore) Close() { + db.DbMap.Db.Close() +} + +// Fetches a session for a given name after it has been added to the registry. +func (db *PGStore) Get(r *http.Request, name string) (*sessions.Session, error) { + return sessions.GetRegistry(r).Get(db, name) +} + +// New returns a new session for the given name w/o adding it to the registry. +func (db *PGStore) New(r *http.Request, name string) (*sessions.Session, error) { + session := sessions.NewSession(db, name) + if session == nil { + return session, nil + } + session.Options = &(*db.Options) + session.IsNew = true + + var err error + if c, errCookie := r.Cookie(name); errCookie == nil { + err = securecookie.DecodeMulti(name, c.Value, &session.ID, db.Codecs...) + if err == nil { + err = db.load(session) + if err == nil { + session.IsNew = false + } + } + } + + return session, err +} + +func (db *PGStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { + // Set delete if max-age is < 0 + if session.Options.MaxAge < 0 { + if err := db.destroy(session); err != nil { + return err + } + http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) + return nil + } + + if session.ID == "" { + // Generate a random session ID key suitable for storage in the DB + session.ID = string(securecookie.GenerateRandomKey(32)) + session.ID = strings.TrimRight( + base32.StdEncoding.EncodeToString( + securecookie.GenerateRandomKey(32)), "=") + } + + if err := db.save(session); err != nil { + return err + } + + // Keep the session ID key in a cookie so it can be looked up in DB later. + encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, db.Codecs...) + if err != nil { + return err + } + + http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options)) + return nil +} + +//load fetches a session by ID from the database and decodes its content into session.Values +func (db *PGStore) load(session *sessions.Session) error { + var s Session + err := db.DbMap.SelectOne(&s, "SELECT * FROM http_sessions WHERE key = $1", session.ID) + + if err := securecookie.DecodeMulti(session.Name(), string(s.Data), + &session.Values, db.Codecs...); err != nil { + return err + } + + return err +} + +// save writes encoded session.Values to a database record. +// writes to http_sessions table by default. +func (db *PGStore) save(session *sessions.Session) error { + encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, + db.Codecs...) + + if err != nil { + return err + } + + var createdOn time.Time + var expiresOn time.Time + + crOn := session.Values["created_on"] + exOn := session.Values["expires_on"] + + if crOn == nil { + createdOn = time.Now() + } else { + createdOn = crOn.(time.Time) + } + + if exOn == nil { + expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge)) + } else { + expiresOn = exOn.(time.Time) + if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 { + expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge)) + } + } + + s := Session{ + Key: session.ID, + Data: encoded, + CreatedOn: createdOn, + ExpiresOn: expiresOn, + ModifiedOn: time.Now(), + } + + if session.IsNew { + err = db.DbMap.Insert(&s) + } else { + _, err = db.DbMap.Update(&s) + } + + return err +} + +// Delete session +func (db *PGStore) destroy(session *sessions.Session) error { + _, err := db.DbMap.Db.Exec("DELETE FROM http_sessions WHERE key = $1", session.ID) + return err +}