2014-01-07 17:15:27 +00:00
|
|
|
package pgstore
|
|
|
|
|
|
|
|
import (
|
|
|
|
"database/sql"
|
|
|
|
"encoding/base32"
|
2015-07-08 11:31:02 +00:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2014-01-07 17:15:27 +00:00
|
|
|
"github.com/coopernurse/gorp"
|
|
|
|
"github.com/gorilla/securecookie"
|
|
|
|
"github.com/gorilla/sessions"
|
2016-07-21 19:48:29 +00:00
|
|
|
|
|
|
|
// Include the pq postgres driver.
|
2014-01-07 17:15:27 +00:00
|
|
|
_ "github.com/lib/pq"
|
|
|
|
)
|
|
|
|
|
2016-07-21 19:48:29 +00:00
|
|
|
// PGStore represents the currently configured session store.
|
2014-01-07 17:15:27 +00:00
|
|
|
type PGStore struct {
|
|
|
|
Codecs []securecookie.Codec
|
|
|
|
Options *sessions.Options
|
|
|
|
path string
|
|
|
|
DbMap *gorp.DbMap
|
|
|
|
}
|
|
|
|
|
|
|
|
// Session type
|
|
|
|
type Session struct {
|
2015-09-07 17:01:16 +00:00
|
|
|
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"`
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
2015-09-17 22:49:25 +00:00
|
|
|
// NewPGStore creates a new PGStore instance and a new database/sql pool.
|
|
|
|
// This will also create in the database the schema needed by pgstore.
|
2016-02-24 12:29:35 +00:00
|
|
|
func NewPGStore(dbURL string, keyPairs ...[]byte) (*PGStore, error) {
|
2015-09-07 17:01:16 +00:00
|
|
|
db, err := sql.Open("postgres", dbURL)
|
2015-09-09 00:39:11 +00:00
|
|
|
if err != nil {
|
2016-07-21 19:48:29 +00:00
|
|
|
// Ignore PGStore value and return nil.
|
2016-02-24 12:29:35 +00:00
|
|
|
return nil, err
|
2015-09-09 00:39:11 +00:00
|
|
|
}
|
|
|
|
return NewPGStoreFromPool(db, keyPairs...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewPGStoreFromPool creates a new PGStore instance from an existing
|
2015-09-17 22:49:25 +00:00
|
|
|
// database/sql pool.
|
|
|
|
// This will also create in the database the schema needed by pgstore.
|
2016-02-24 12:29:35 +00:00
|
|
|
func NewPGStoreFromPool(db *sql.DB, keyPairs ...[]byte) (*PGStore, error) {
|
2014-01-07 17:15:27 +00:00
|
|
|
dbmap := &gorp.DbMap{Db: db, Dialect: gorp.PostgresDialect{}}
|
|
|
|
|
|
|
|
dbStore := &PGStore{
|
|
|
|
Codecs: securecookie.CodecsFromPairs(keyPairs...),
|
|
|
|
Options: &sessions.Options{
|
|
|
|
Path: "/",
|
|
|
|
MaxAge: 86400 * 30,
|
|
|
|
},
|
|
|
|
DbMap: dbmap,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create table if it doesn't exist
|
|
|
|
dbmap.AddTableWithName(Session{}, "http_sessions").SetKeys(true, "Id")
|
2015-09-09 00:39:11 +00:00
|
|
|
err := dbmap.CreateTablesIfNotExists()
|
2014-01-07 17:15:27 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2016-07-21 19:48:29 +00:00
|
|
|
// Ignore PGStore value and return nil.
|
2016-02-24 12:29:35 +00:00
|
|
|
return nil, err
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
2016-02-24 12:29:35 +00:00
|
|
|
return dbStore, nil
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
2016-07-21 19:48:29 +00:00
|
|
|
// Close closes the database connection.
|
2014-01-07 17:15:27 +00:00
|
|
|
func (db *PGStore) Close() {
|
|
|
|
db.DbMap.Db.Close()
|
|
|
|
}
|
|
|
|
|
2015-09-07 17:01:16 +00:00
|
|
|
// Get Fetches a session for a given name after it has been added to the
|
|
|
|
// registry.
|
2014-01-07 17:15:27 +00:00
|
|
|
func (db *PGStore) Get(r *http.Request, name string) (*sessions.Session, error) {
|
|
|
|
return sessions.GetRegistry(r).Get(db, name)
|
|
|
|
}
|
|
|
|
|
2016-07-21 19:48:29 +00:00
|
|
|
// New returns a new session for the given name without adding it to the registry.
|
2014-01-07 17:15:27 +00:00
|
|
|
func (db *PGStore) New(r *http.Request, name string) (*sessions.Session, error) {
|
|
|
|
session := sessions.NewSession(db, name)
|
|
|
|
if session == nil {
|
2016-07-21 19:48:29 +00:00
|
|
|
return nil, nil
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
2016-07-21 19:48:29 +00:00
|
|
|
|
2015-05-06 16:10:11 +00:00
|
|
|
opts := *db.Options
|
|
|
|
session.Options = &(opts)
|
2014-01-07 17:15:27 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-11 12:13:41 +00:00
|
|
|
db.MaxAge(db.Options.MaxAge)
|
|
|
|
|
2014-01-07 17:15:27 +00:00
|
|
|
return session, err
|
|
|
|
}
|
|
|
|
|
2015-09-07 17:01:16 +00:00
|
|
|
// Save saves the given session into the database and deletes cookies if needed
|
2014-01-07 17:15:27 +00:00
|
|
|
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 = strings.TrimRight(
|
|
|
|
base32.StdEncoding.EncodeToString(
|
2016-07-21 19:48:29 +00:00
|
|
|
securecookie.GenerateRandomKey(32),
|
|
|
|
), "=")
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2015-07-08 11:31:02 +00:00
|
|
|
// MaxLength restricts the maximum length of new sessions to l.
|
|
|
|
// If l is 0 there is no limit to the size of a session, use with caution.
|
|
|
|
// The default for a new PGStore is 4096. PostgreSQL allows for max
|
|
|
|
// value sizes of up to 1GB (http://www.postgresql.org/docs/current/interactive/datatype-character.html)
|
2015-09-07 17:01:16 +00:00
|
|
|
func (db *PGStore) MaxLength(l int) {
|
|
|
|
for _, c := range db.Codecs {
|
2015-07-08 11:31:02 +00:00
|
|
|
if codec, ok := c.(*securecookie.SecureCookie); ok {
|
|
|
|
codec.MaxLength(l)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-08-11 12:13:41 +00:00
|
|
|
// MaxAge sets the maximum age for the store and the underlying cookie
|
|
|
|
// implementation. Individual sessions can be deleted by setting Options.MaxAge
|
|
|
|
// = -1 for that session.
|
|
|
|
func (db *PGStore) MaxAge(age int) {
|
|
|
|
db.Options.MaxAge = age
|
|
|
|
|
|
|
|
// Set the maxAge for each securecookie instance.
|
|
|
|
for _, codec := range db.Codecs {
|
|
|
|
if sc, ok := codec.(*securecookie.SecureCookie); ok {
|
|
|
|
sc.MaxAge(age)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-09-07 17:01:16 +00:00
|
|
|
// load fetches a session by ID from the database and decodes its content
|
2016-07-21 19:48:29 +00:00
|
|
|
// into session.Values.
|
2014-01-07 17:15:27 +00:00
|
|
|
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)
|
2016-07-21 19:48:29 +00:00
|
|
|
if err != nil {
|
2014-01-07 17:15:27 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-07-21 19:48:29 +00:00
|
|
|
return securecookie.DecodeMulti(session.Name(), string(s.Data), &session.Values, db.Codecs...)
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// save writes encoded session.Values to a database record.
|
|
|
|
// writes to http_sessions table by default.
|
|
|
|
func (db *PGStore) save(session *sessions.Session) error {
|
2016-07-21 19:48:29 +00:00
|
|
|
encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, db.Codecs...)
|
2014-01-07 17:15:27 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
crOn := session.Values["created_on"]
|
|
|
|
exOn := session.Values["expires_on"]
|
|
|
|
|
2016-07-21 19:48:29 +00:00
|
|
|
var expiresOn time.Time
|
|
|
|
|
|
|
|
createdOn, ok := crOn.(time.Time)
|
|
|
|
if !ok {
|
2014-01-07 17:15:27 +00:00
|
|
|
createdOn = time.Now()
|
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2016-07-21 19:48:29 +00:00
|
|
|
return db.DbMap.Insert(&s)
|
2014-01-07 17:15:27 +00:00
|
|
|
}
|
|
|
|
|
2016-07-21 19:48:29 +00:00
|
|
|
_, err = db.DbMap.Exec("update http_sessions set data=$1, modified_on=$2, expires_on=$3 where key=$4", s.Data, s.ModifiedOn, s.ExpiresOn, s.Key)
|
2014-01-07 17:15:27 +00:00
|
|
|
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
|
|
|
|
}
|