sessionstore.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. // Package sessionstore is a postgresql backend implementation of gorilla/sessions Session interface, based on
  2. // antonlindstrom/pgstore. Key change is to use GORM instead of typical sql driver using queries.
  3. package sessionstore
  4. import (
  5. "encoding/base32"
  6. "net/http"
  7. "strings"
  8. "time"
  9. "github.com/gorilla/securecookie"
  10. "github.com/gorilla/sessions"
  11. "github.com/porter-dev/porter/api/server/shared/config/env"
  12. "github.com/porter-dev/porter/internal/models"
  13. "github.com/porter-dev/porter/internal/repository"
  14. "gorm.io/gorm"
  15. )
  16. // structs
  17. // PGStore is a wrapper around gorilla/sessions store.
  18. type PGStore struct {
  19. Codecs []securecookie.Codec
  20. Options *sessions.Options
  21. Path string
  22. Repo repository.SessionRepository
  23. }
  24. // Helpers
  25. // MaxLength restricts the maximum length of new sessions to l.
  26. // If l is 0 there is no limit to the size of a session, use with caution.
  27. // The default for a new PGStore is 4096. PostgreSQL allows for max
  28. // value sizes of up to 1GB (http://www.postgresql.org/docs/current/interactive/datatype-character.html)
  29. func (store *PGStore) MaxLength(l int) {
  30. for _, c := range store.Codecs {
  31. if codec, ok := c.(*securecookie.SecureCookie); ok {
  32. codec.MaxLength(l)
  33. }
  34. }
  35. }
  36. // MaxAge sets the maximum age for the store and the underlying cookie
  37. // implementation. Individual sessions can be deleted by setting Options.MaxAge
  38. // = -1 for that session.
  39. func (store *PGStore) MaxAge(age int) {
  40. store.Options.MaxAge = age
  41. // Set the maxAge for each securecookie instance.
  42. for _, codec := range store.Codecs {
  43. if sc, ok := codec.(*securecookie.SecureCookie); ok {
  44. sc.MaxAge(age)
  45. }
  46. }
  47. }
  48. // load fetches a session by ID from the database and decodes its content
  49. // into session.Values.
  50. func (store *PGStore) load(session *sessions.Session) error {
  51. res, err := store.Repo.SelectSession(&models.Session{Key: session.ID})
  52. if err != nil {
  53. return err
  54. }
  55. return securecookie.DecodeMulti(session.Name(), string(res.Data), &session.Values, store.Codecs...)
  56. }
  57. // save writes encoded session.Values to a database record.
  58. // writes to http_sessions table by default.
  59. func (store *PGStore) save(session *sessions.Session) error {
  60. encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, store.Codecs...)
  61. if err != nil {
  62. return err
  63. }
  64. exOn := session.Values["expires_on"]
  65. var expiresOn time.Time
  66. if exOn == nil {
  67. expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
  68. } else {
  69. expiresOn = exOn.(time.Time)
  70. if expiresOn.Sub(time.Now().Add(time.Second*time.Duration(session.Options.MaxAge))) < 0 {
  71. expiresOn = time.Now().Add(time.Second * time.Duration(session.Options.MaxAge))
  72. }
  73. }
  74. s := &models.Session{
  75. Key: session.ID,
  76. Data: []byte(encoded),
  77. ExpiresAt: expiresOn,
  78. }
  79. repo := store.Repo
  80. if session.IsNew {
  81. _, createErr := repo.CreateSession(s)
  82. return createErr
  83. }
  84. _, updateErr := repo.UpdateSession(s)
  85. return updateErr
  86. }
  87. // Implementation of the interface (Get, New, Save)
  88. type NewStoreOpts struct {
  89. SessionRepository repository.SessionRepository
  90. CookieSecrets []string
  91. Insecure bool
  92. }
  93. // NewStore takes an initialized db and session key pairs to create a session-store in postgres db.
  94. func NewStore(opts *NewStoreOpts) (*PGStore, error) {
  95. keyPairs := [][]byte{}
  96. for _, key := range opts.CookieSecrets {
  97. keyPairs = append(keyPairs, []byte(key))
  98. }
  99. dbStore := &PGStore{
  100. Codecs: securecookie.CodecsFromPairs(keyPairs...),
  101. Options: &sessions.Options{
  102. Path: "/",
  103. MaxAge: 86400 * 30,
  104. Secure: !opts.Insecure,
  105. HttpOnly: true,
  106. SameSite: http.SameSiteLaxMode,
  107. },
  108. Repo: opts.SessionRepository,
  109. }
  110. return dbStore, nil
  111. }
  112. // NewFilesystemStore takes session key pairs to create a session-store in the local fs without using a db.
  113. func NewFilesystemStore(conf env.ServerConf) *sessions.FilesystemStore {
  114. keyPairs := [][]byte{}
  115. for _, key := range conf.CookieSecrets {
  116. keyPairs = append(keyPairs, []byte(key))
  117. }
  118. // Defaults to os.TempDir() when first argument (path) isn't specified.
  119. store := sessions.NewFilesystemStore("", keyPairs...)
  120. return store
  121. }
  122. // Get Fetches a session for a given name after it has been added to the
  123. // registry.
  124. func (store *PGStore) Get(r *http.Request, name string) (*sessions.Session, error) {
  125. return sessions.GetRegistry(r).Get(store, name)
  126. }
  127. // New returns a new session for the given name without adding it to the registry.
  128. func (store *PGStore) New(r *http.Request, name string) (*sessions.Session, error) {
  129. session := sessions.NewSession(store, name)
  130. if session == nil {
  131. return nil, nil
  132. }
  133. opts := *store.Options
  134. session.Options = &(opts)
  135. session.IsNew = true
  136. var err error
  137. if c, errCookie := r.Cookie(name); errCookie == nil {
  138. err = securecookie.DecodeMulti(name, c.Value, &session.ID, store.Codecs...)
  139. if err == nil {
  140. err = store.load(session)
  141. if err != nil {
  142. if err == gorm.ErrRecordNotFound {
  143. err = nil
  144. } else if strings.Contains(err.Error(), "expired timestamp") {
  145. err = nil
  146. session.IsNew = false
  147. }
  148. } else {
  149. session.IsNew = false
  150. }
  151. }
  152. }
  153. store.MaxAge(store.Options.MaxAge)
  154. return session, err
  155. }
  156. // Save saves the given session into the database and deletes cookies if needed
  157. func (store *PGStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error {
  158. repo := store.Repo
  159. // Set delete if max-age is < 0
  160. if session.Options.MaxAge < 0 {
  161. if _, err := repo.DeleteSession(&models.Session{Key: session.ID}); err != nil {
  162. return err
  163. }
  164. http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options))
  165. return nil
  166. }
  167. if session.ID == "" {
  168. // Generate a random session ID key suitable for storage in the DB
  169. session.ID = strings.TrimRight(
  170. base32.StdEncoding.EncodeToString(
  171. securecookie.GenerateRandomKey(32),
  172. ), "=")
  173. }
  174. if err := store.save(session); err != nil {
  175. return err
  176. }
  177. // Keep the session ID key in a cookie so it can be looked up in DB later.
  178. encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, store.Codecs...)
  179. if err != nil {
  180. return err
  181. }
  182. http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options))
  183. return nil
  184. }