You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

xormstore.go 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /*
  2. Package xormstore is a XORM backend for gorilla sessions
  3. Simplest form:
  4. store, err := xormstore.New(engine, []byte("secret-hash-key"))
  5. All options:
  6. store, err := xormstore.NewOptions(
  7. engine, // *xorm.Engine
  8. xormstore.Options{
  9. TableName: "sessions", // "sessions" is default
  10. SkipCreateTable: false, // false is default
  11. },
  12. []byte("secret-hash-key"), // 32 or 64 bytes recommended, required
  13. []byte("secret-encyption-key")) // nil, 16, 24 or 32 bytes, optional
  14. if err != nil {
  15. // xormstore can not be initialized
  16. }
  17. // some more settings, see sessions.Options
  18. store.SessionOpts.Secure = true
  19. store.SessionOpts.HttpOnly = true
  20. store.SessionOpts.MaxAge = 60 * 60 * 24 * 60
  21. If you want periodic cleanup of expired sessions:
  22. quit := make(chan struct{})
  23. go store.PeriodicCleanup(1*time.Hour, quit)
  24. For more information about the keys see https://github.com/gorilla/securecookie
  25. For API to use in HTTP handlers see https://github.com/gorilla/sessions
  26. */
  27. package xormstore
  28. import (
  29. "encoding/base32"
  30. "net/http"
  31. "strings"
  32. "time"
  33. "github.com/lafriks/xormstore/util"
  34. "github.com/go-xorm/xorm"
  35. "github.com/gorilla/context"
  36. "github.com/gorilla/securecookie"
  37. "github.com/gorilla/sessions"
  38. )
  39. const sessionIDLen = 32
  40. const defaultTableName = "sessions"
  41. const defaultMaxAge = 60 * 60 * 24 * 30 // 30 days
  42. const defaultPath = "/"
  43. // Options for xormstore
  44. type Options struct {
  45. TableName string
  46. SkipCreateTable bool
  47. }
  48. // Store represent a xormstore
  49. type Store struct {
  50. e *xorm.Engine
  51. opts Options
  52. Codecs []securecookie.Codec
  53. SessionOpts *sessions.Options
  54. }
  55. type xormSession struct {
  56. ID string `xorm:"VARCHAR(100) PK NAME 'id'"`
  57. Data string `xorm:"TEXT"`
  58. CreatedUnix util.TimeStamp `xorm:"created"`
  59. UpdatedUnix util.TimeStamp `xorm:"updated"`
  60. ExpiresUnix util.TimeStamp `xorm:"INDEX"`
  61. tableName string `xorm:"-"` // just to store table name for easier access
  62. }
  63. // Define a type for context keys so that they can't clash with anything else stored in context
  64. type contextKey string
  65. func (xs *xormSession) TableName() string {
  66. return xs.tableName
  67. }
  68. // New creates a new xormstore session
  69. func New(e *xorm.Engine, keyPairs ...[]byte) (*Store, error) {
  70. return NewOptions(e, Options{}, keyPairs...)
  71. }
  72. // NewOptions creates a new xormstore session with options
  73. func NewOptions(e *xorm.Engine, opts Options, keyPairs ...[]byte) (*Store, error) {
  74. st := &Store{
  75. e: e,
  76. opts: opts,
  77. Codecs: securecookie.CodecsFromPairs(keyPairs...),
  78. SessionOpts: &sessions.Options{
  79. Path: defaultPath,
  80. MaxAge: defaultMaxAge,
  81. },
  82. }
  83. if st.opts.TableName == "" {
  84. st.opts.TableName = defaultTableName
  85. }
  86. if !st.opts.SkipCreateTable {
  87. if err := st.e.Sync2(&xormSession{tableName: st.opts.TableName}); err != nil {
  88. return nil, err
  89. }
  90. }
  91. return st, nil
  92. }
  93. // Get returns a session for the given name after adding it to the registry.
  94. func (st *Store) Get(r *http.Request, name string) (*sessions.Session, error) {
  95. return sessions.GetRegistry(r).Get(st, name)
  96. }
  97. // New creates a session with name without adding it to the registry.
  98. func (st *Store) New(r *http.Request, name string) (*sessions.Session, error) {
  99. session := sessions.NewSession(st, name)
  100. opts := *st.SessionOpts
  101. session.Options = &opts
  102. st.MaxAge(st.SessionOpts.MaxAge)
  103. // try fetch from db if there is a cookie
  104. if cookie, err := r.Cookie(name); err == nil {
  105. if err := securecookie.DecodeMulti(name, cookie.Value, &session.ID, st.Codecs...); err != nil {
  106. return session, nil
  107. }
  108. s := &xormSession{tableName: st.opts.TableName}
  109. if has, err := st.e.Where("id = ? AND expires_unix >= ?", session.ID, util.TimeStampNow()).Get(s); !has || err != nil {
  110. return session, nil
  111. }
  112. if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil {
  113. return session, nil
  114. }
  115. context.Set(r, contextKey(name), s)
  116. }
  117. return session, nil
  118. }
  119. // Save session and set cookie header
  120. func (st *Store) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
  121. s, _ := context.Get(r, contextKey(session.Name())).(*xormSession)
  122. // delete if max age is < 0
  123. if session.Options.MaxAge < 0 {
  124. if s != nil {
  125. if _, err := st.e.Delete(&xormSession{
  126. ID: session.ID,
  127. tableName: st.opts.TableName,
  128. }); err != nil {
  129. return err
  130. }
  131. }
  132. http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
  133. return nil
  134. }
  135. data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...)
  136. if err != nil {
  137. return err
  138. }
  139. now := util.TimeStampNow()
  140. expire := now.AddDuration(time.Second * time.Duration(session.Options.MaxAge))
  141. if s == nil {
  142. // generate random session ID key suitable for storage in the db
  143. session.ID = strings.TrimRight(
  144. base32.StdEncoding.EncodeToString(
  145. securecookie.GenerateRandomKey(sessionIDLen)), "=")
  146. s = &xormSession{
  147. ID: session.ID,
  148. Data: data,
  149. CreatedUnix: now,
  150. UpdatedUnix: now,
  151. ExpiresUnix: expire,
  152. tableName: st.opts.TableName,
  153. }
  154. if _, err := st.e.Insert(s); err != nil {
  155. return err
  156. }
  157. context.Set(r, contextKey(session.Name()), s)
  158. } else {
  159. s.Data = data
  160. s.UpdatedUnix = now
  161. s.ExpiresUnix = expire
  162. if _, err := st.e.ID(s.ID).Cols("data", "updated_unix", "expires_unix").Update(s); err != nil {
  163. return err
  164. }
  165. }
  166. // set session id cookie
  167. id, err := securecookie.EncodeMulti(session.Name(), session.ID, st.Codecs...)
  168. if err != nil {
  169. return err
  170. }
  171. http.SetCookie(w, sessions.NewCookie(session.Name(), id, session.Options))
  172. return nil
  173. }
  174. // MaxAge sets the maximum age for the store and the underlying cookie
  175. // implementation. Individual sessions can be deleted by setting
  176. // Options.MaxAge = -1 for that session.
  177. func (st *Store) MaxAge(age int) {
  178. st.SessionOpts.MaxAge = age
  179. for _, codec := range st.Codecs {
  180. if sc, ok := codec.(*securecookie.SecureCookie); ok {
  181. sc.MaxAge(age)
  182. }
  183. }
  184. }
  185. // MaxLength restricts the maximum length of new sessions to l.
  186. // If l is 0 there is no limit to the size of a session, use with caution.
  187. // The default is 4096 (default for securecookie)
  188. func (st *Store) MaxLength(l int) {
  189. for _, c := range st.Codecs {
  190. if codec, ok := c.(*securecookie.SecureCookie); ok {
  191. codec.MaxLength(l)
  192. }
  193. }
  194. }
  195. // Cleanup deletes expired sessions
  196. func (st *Store) Cleanup() {
  197. st.e.Where("expires_unix < ?", util.TimeStampNow()).Delete(&xormSession{tableName: st.opts.TableName})
  198. }
  199. // PeriodicCleanup runs Cleanup every interval. Close quit channel to stop.
  200. func (st *Store) PeriodicCleanup(interval time.Duration, quit <-chan struct{}) {
  201. t := time.NewTicker(interval)
  202. defer t.Stop()
  203. for {
  204. select {
  205. case <-t.C:
  206. st.Cleanup()
  207. case <-quit:
  208. return
  209. }
  210. }
  211. }