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