sessions.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. package sessions
  2. import (
  3. "net/http"
  4. "time"
  5. "github.com/kataras/iris/context"
  6. )
  7. func init() {
  8. context.SetHandlerName("iris/sessions.*Handler", "iris.session")
  9. }
  10. // A Sessions manager should be responsible to Start/Get a sesion, based
  11. // on a Context, which returns a *Session, type.
  12. // It performs automatic memory cleanup on expired sessions.
  13. // It can accept a `Database` for persistence across server restarts.
  14. // A session can set temporary values (flash messages).
  15. type Sessions struct {
  16. config Config
  17. provider *provider
  18. cookieOptions []context.CookieOption // options added on each session cookie action.
  19. }
  20. // New returns a new fast, feature-rich sessions manager
  21. // it can be adapted to an iris station
  22. func New(cfg Config) *Sessions {
  23. var cookieOptions []context.CookieOption
  24. if cfg.AllowReclaim {
  25. cookieOptions = append(cookieOptions, context.CookieAllowReclaim(cfg.Cookie))
  26. }
  27. if !cfg.DisableSubdomainPersistence {
  28. cookieOptions = append(cookieOptions, context.CookieAllowSubdomains(cfg.Cookie))
  29. }
  30. if cfg.CookieSecureTLS {
  31. cookieOptions = append(cookieOptions, context.CookieSecure)
  32. }
  33. if cfg.Encoding != nil {
  34. cookieOptions = append(cookieOptions, context.CookieEncoding(cfg.Encoding, cfg.Cookie))
  35. }
  36. return &Sessions{
  37. cookieOptions: cookieOptions,
  38. config: cfg.Validate(),
  39. provider: newProvider(),
  40. }
  41. }
  42. // UseDatabase adds a session database to the manager's provider,
  43. // a session db doesn't have write access
  44. func (s *Sessions) UseDatabase(db Database) {
  45. s.provider.RegisterDatabase(db)
  46. }
  47. // GetCookieOptions returns the cookie options registered
  48. // for this sessions manager based on the configuration.
  49. func (s *Sessions) GetCookieOptions() []context.CookieOption {
  50. return s.cookieOptions
  51. }
  52. // updateCookie gains the ability of updating the session browser cookie to any method which wants to update it
  53. func (s *Sessions) updateCookie(ctx *context.Context, sid string, expires time.Duration, options ...context.CookieOption) {
  54. cookie := &http.Cookie{}
  55. // The RFC makes no mention of encoding url value, so here I think to encode both sessionid key and the value using the safe(to put and to use as cookie) url-encoding
  56. cookie.Name = s.config.Cookie
  57. cookie.Value = sid
  58. cookie.Path = "/"
  59. cookie.HttpOnly = true
  60. // MaxAge=0 means no 'Max-Age' attribute specified.
  61. // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
  62. // MaxAge>0 means Max-Age attribute present and given in seconds
  63. if expires >= 0 {
  64. if expires == 0 { // unlimited life
  65. cookie.Expires = context.CookieExpireUnlimited
  66. } else { // > 0
  67. cookie.Expires = time.Now().Add(expires)
  68. }
  69. cookie.MaxAge = int(time.Until(cookie.Expires).Seconds())
  70. }
  71. s.upsertCookie(ctx, cookie, options)
  72. }
  73. func (s *Sessions) upsertCookie(ctx *context.Context, cookie *http.Cookie, cookieOptions []context.CookieOption) {
  74. opts := s.cookieOptions
  75. if len(cookieOptions) > 0 {
  76. opts = append(opts, cookieOptions...)
  77. }
  78. ctx.UpsertCookie(cookie, opts...)
  79. }
  80. func (s *Sessions) getCookie(ctx *context.Context, cookieOptions []context.CookieOption) string {
  81. opts := s.cookieOptions
  82. if len(cookieOptions) > 0 {
  83. opts = append(opts, cookieOptions...)
  84. }
  85. return ctx.GetCookie(s.config.Cookie, opts...)
  86. }
  87. // Start creates or retrieves an existing session for the particular request.
  88. // Note that `Start` method will not respect configuration's `AllowReclaim`, `DisableSubdomainPersistence`, `CookieSecureTLS`,
  89. // and `Encoding` settings.
  90. // Register sessions as a middleware through the `Handler` method instead,
  91. // which provides automatic resolution of a *sessions.Session input argument
  92. // on MVC and APIContainer as well.
  93. //
  94. // NOTE: Use `app.Use(sess.Handler())` instead, avoid using `Start` manually.
  95. func (s *Sessions) Start(ctx *context.Context, cookieOptions ...context.CookieOption) *Session {
  96. cookieValue := s.getCookie(ctx, cookieOptions)
  97. if cookieValue == "" { // cookie doesn't exist, let's generate a session and set a cookie.
  98. sid := s.config.SessionIDGenerator(ctx)
  99. sess := s.provider.Init(s, sid, s.config.Expires)
  100. // n := s.provider.db.Len(sid)
  101. // fmt.Printf("db.Len(%s) = %d\n", sid, n)
  102. // if n > 0 {
  103. // s.provider.db.Visit(sid, func(key string, value interface{}) {
  104. // fmt.Printf("%s=%s\n", key, value)
  105. // })
  106. // }
  107. sess.isNew = s.provider.db.Len(sid) == 0
  108. s.updateCookie(ctx, sid, s.config.Expires, cookieOptions...)
  109. return sess
  110. }
  111. return s.provider.Read(s, cookieValue, s.config.Expires)
  112. }
  113. const sessionContextKey = "iris.session"
  114. // Handler returns a sessions middleware to register on application routes.
  115. // To return the request's Session call the `Get(ctx)` package-level function.
  116. //
  117. // Call `Handler()` once per sessions manager.
  118. func (s *Sessions) Handler(requestOptions ...context.CookieOption) context.Handler {
  119. return func(ctx *context.Context) {
  120. session := s.Start(ctx, requestOptions...) // this cookie's end-developer's custom options.
  121. ctx.Values().Set(sessionContextKey, session)
  122. ctx.Next()
  123. }
  124. }
  125. // Get returns a *Session from the same request life cycle,
  126. // can be used inside a chain of handlers of a route.
  127. //
  128. // The `Sessions.Start` should be called previously,
  129. // e.g. register the `Sessions.Handler` as middleware.
  130. // Then call `Get` package-level function as many times as you want.
  131. // Note: It will return nil if the session got destroyed by the same request.
  132. // If you need to destroy and start a new session in the same request you need to call
  133. // sessions manager's `Start` method after Destroy.
  134. func Get(ctx *context.Context) *Session {
  135. if v := ctx.Values().Get(sessionContextKey); v != nil {
  136. if sess, ok := v.(*Session); ok {
  137. return sess
  138. }
  139. }
  140. // ctx.Application().Logger().Debugf("Sessions: Get: no session found, prior Destroy(ctx) calls in the same request should follow with a Start(ctx) call too")
  141. return nil
  142. }
  143. // StartWithPath same as `Start` but it explicitly accepts the cookie path option.
  144. func (s *Sessions) StartWithPath(ctx *context.Context, path string) *Session {
  145. return s.Start(ctx, context.CookiePath(path))
  146. }
  147. // ShiftExpiration move the expire date of a session to a new date
  148. // by using session default timeout configuration.
  149. // It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet.
  150. func (s *Sessions) ShiftExpiration(ctx *context.Context, cookieOptions ...context.CookieOption) error {
  151. return s.UpdateExpiration(ctx, s.config.Expires, cookieOptions...)
  152. }
  153. // UpdateExpiration change expire date of a session to a new date
  154. // by using timeout value passed by `expires` receiver.
  155. // It will return `ErrNotFound` when trying to update expiration on a non-existence or not valid session entry.
  156. // It will return `ErrNotImplemented` if a database is used and it does not support this feature, yet.
  157. func (s *Sessions) UpdateExpiration(ctx *context.Context, expires time.Duration, cookieOptions ...context.CookieOption) error {
  158. cookieValue := s.getCookie(ctx, cookieOptions)
  159. if cookieValue == "" {
  160. return ErrNotFound
  161. }
  162. // we should also allow it to expire when the browser closed
  163. err := s.provider.UpdateExpiration(cookieValue, expires)
  164. if err == nil || expires == -1 {
  165. s.updateCookie(ctx, cookieValue, expires, cookieOptions...)
  166. }
  167. return err
  168. }
  169. // DestroyListener is the form of a destroy listener.
  170. // Look `OnDestroy` for more.
  171. type DestroyListener func(sid string)
  172. // OnDestroy registers one or more destroy listeners.
  173. // A destroy listener is fired when a session has been removed entirely from the server (the entry) and client-side (the cookie).
  174. // Note that if a destroy listener is blocking, then the session manager will delay respectfully,
  175. // use a goroutine inside the listener to avoid that behavior.
  176. func (s *Sessions) OnDestroy(listeners ...DestroyListener) {
  177. for _, ln := range listeners {
  178. s.provider.registerDestroyListener(ln)
  179. }
  180. }
  181. // Destroy removes the session data, the associated cookie
  182. // and the Context's session value.
  183. // Next calls of `sessions.Get` will occur to a nil Session,
  184. // use `Sessions#Start` method for renewal
  185. // or use the Session's Destroy method which does keep the session entry with its values cleared.
  186. func (s *Sessions) Destroy(ctx *context.Context) {
  187. cookieValue := s.getCookie(ctx, nil)
  188. if cookieValue == "" { // nothing to destroy
  189. return
  190. }
  191. ctx.Values().Remove(sessionContextKey)
  192. ctx.RemoveCookie(s.config.Cookie)
  193. s.provider.Destroy(cookieValue)
  194. }
  195. // DestroyByID removes the session entry
  196. // from the server-side memory (and database if registered).
  197. // Client's session cookie will still exist but it will be reseted on the next request.
  198. //
  199. // It's safe to use it even if you are not sure if a session with that id exists.
  200. //
  201. // Note: the sid should be the original one (i.e: fetched by a store )
  202. // it's not decoded.
  203. func (s *Sessions) DestroyByID(sid string) {
  204. s.provider.Destroy(sid)
  205. }
  206. // DestroyAll removes all sessions
  207. // from the server-side memory (and database if registered).
  208. // Client's session cookie will still exist but it will be reseted on the next request.
  209. func (s *Sessions) DestroyAll() {
  210. s.provider.DestroyAll()
  211. }