sessions.go 10 KB

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