forked from kataras/iris
-
Notifications
You must be signed in to change notification settings - Fork 0
/
sessions.go
404 lines (352 loc) · 14 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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
package iris
import (
"container/list"
"encoding/base64"
"strconv"
"strings"
"sync"
"time"
"github.com/kataras/iris/config"
"github.com/kataras/iris/utils"
"github.com/valyala/fasthttp"
)
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------SessionDatabase implementation---------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// SessionDatabase is the interface which all session databases should implement
// By design it doesn't support any type of cookie store like other frameworks, I want to protect you, believe me, no context access (although we could)
// The scope of the database is to store somewhere the sessions in order to keep them after restarting the server, nothing more.
// the values are stored by the underline session, the check for new sessions, or 'this session value should added' are made automatically by Iris, you are able just to set the values to your backend database with Load function.
// session database doesn't have any write or read access to the session, the loading of the initial data is done by the Load(string) map[string]interfface{} function
// synchronization are made automatically, you can register more than one session database but the first non-empty Load return data will be used as the session values.
type SessionDatabase interface {
Load(string) map[string]interface{}
Update(string, map[string]interface{})
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------Session implementation-----------------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// session is an 'object' which wraps the session provider with its session databases, only frontend user has access to this session object.
// this is really used on context and everywhere inside Iris
type session struct {
sid string
values map[string]interface{} // here is the real values
mu sync.Mutex
lastAccessedTime time.Time
createdAt time.Time
provider *sessionProvider
}
// ID returns the session's id
func (s *session) ID() string {
return s.sid
}
// Get returns the value of an entry by its key
func (s *session) Get(key string) interface{} {
s.provider.update(s.sid)
if value, found := s.values[key]; found {
return value
}
return nil
}
// GetString same as Get but returns as string, if nil then returns an empty string
func (s *session) GetString(key string) string {
if value := s.Get(key); value != nil {
if v, ok := value.(string); ok {
return v
}
}
return ""
}
// GetInt same as Get but returns as int, if nil then returns -1
func (s *session) GetInt(key string) int {
if value := s.Get(key); value != nil {
if v, ok := value.(int); ok {
return v
}
}
return -1
}
// GetAll returns all session's values
func (s *session) GetAll() map[string]interface{} {
return s.values
}
// VisitAll loop each one entry and calls the callback function func(key,value)
func (s *session) VisitAll(cb func(k string, v interface{})) {
for key := range s.values {
cb(key, s.values[key])
}
}
// Set fills the session with an entry, it receives a key and a value
// returns an error, which is always nil
func (s *session) Set(key string, value interface{}) {
s.mu.Lock()
s.values[key] = value
s.mu.Unlock()
s.provider.update(s.sid)
}
// Delete removes an entry by its key
// returns an error, which is always nil
func (s *session) Delete(key string) {
s.mu.Lock()
delete(s.values, key)
s.mu.Unlock()
s.provider.update(s.sid)
}
// Clear removes all entries
func (s *session) Clear() {
s.mu.Lock()
for key := range s.values {
delete(s.values, key)
}
s.mu.Unlock()
s.provider.update(s.sid)
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------sessionProvider implementation---------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
type (
// sessionProvider contains the temp sessions memory and the databases
sessionProvider struct {
mu sync.Mutex
sessions map[string]*list.Element // underline TEMPORARY memory store used to give advantage on sessions used more times than others
list *list.List // for GC
databases []SessionDatabase
expires time.Duration
}
)
func (p *sessionProvider) registerDatabase(db SessionDatabase) {
p.mu.Lock() // for any case
p.databases = append(p.databases, db)
p.mu.Unlock()
}
func (p *sessionProvider) newSession(sid string) *session {
sess := &session{
sid: sid,
provider: p,
lastAccessedTime: time.Now(),
values: p.loadSessionValues(sid),
}
if p.expires > 0 { // if not unlimited life duration and no -1 (cookie remove action is based on browser's session)
time.AfterFunc(p.expires, func() {
// the destroy makes the check if this session is exists then or not,
// this is used to destroy the session from the server-side also
// it's good to have here for security reasons, I didn't add it on the gc function to separate its action
p.destroy(sid)
})
}
return sess
}
func (p *sessionProvider) loadSessionValues(sid string) map[string]interface{} {
for i, n := 0, len(p.databases); i < n; i++ {
if dbValues := p.databases[i].Load(sid); dbValues != nil && len(dbValues) > 0 {
return dbValues // return the first non-empty from the registered stores.
}
}
values := make(map[string]interface{})
return values
}
func (p *sessionProvider) updateDatabases(sid string, newValues map[string]interface{}) {
for i, n := 0, len(p.databases); i < n; i++ {
p.databases[i].Update(sid, newValues)
}
}
// Init creates the session and returns it
func (p *sessionProvider) init(sid string) *session {
newSession := p.newSession(sid)
elem := p.list.PushBack(newSession)
p.mu.Lock()
p.sessions[sid] = elem
p.mu.Unlock()
return newSession
}
// Read returns the store which sid parameter is belongs
func (p *sessionProvider) read(sid string) *session {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
p.mu.Unlock() // yes defer is slow
elem.Value.(*session).lastAccessedTime = time.Now()
return elem.Value.(*session)
}
p.mu.Unlock()
// if not found create new
sess := p.init(sid)
return sess
}
// Destroy destroys the session, removes all sessions values, the session itself and updates the registered session databases, this called from sessionManager which removes the client's cookie also.
func (p *sessionProvider) destroy(sid string) {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
sess := elem.Value.(*session)
sess.values = nil
p.updateDatabases(sid, nil)
delete(p.sessions, sid)
p.list.Remove(elem)
}
p.mu.Unlock()
}
// Update updates the lastAccessedTime, and moves the memory place element to the front
// always returns a nil error, for now
func (p *sessionProvider) update(sid string) {
p.mu.Lock()
if elem, found := p.sessions[sid]; found {
sess := elem.Value.(*session)
sess.lastAccessedTime = time.Now()
p.list.MoveToFront(elem)
p.updateDatabases(sid, sess.values)
}
p.mu.Unlock()
}
// GC clears the memory
func (p *sessionProvider) gc(duration time.Duration) {
p.mu.Lock()
defer p.mu.Unlock()
for {
elem := p.list.Back()
if elem == nil {
break
}
// if the time has passed. session was expired, then delete the session and its memory place
// we are not destroy the session completely for the case this is re-used after
sess := elem.Value.(*session)
if time.Now().After(sess.lastAccessedTime.Add(duration)) {
p.list.Remove(elem)
} else {
break
}
}
}
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
// ----------------------------------sessionsManager implementation---------------------
// -------------------------------------------------------------------------------------
// -------------------------------------------------------------------------------------
type (
// sessionsManager implements the ISessionsManager interface
// contains the cookie's name, the provider and a duration for GC and cookie life expire
sessionsManager struct {
config *config.Sessions
provider *sessionProvider
}
)
// newSessionsManager creates & returns a new SessionsManager and start its GC
func newSessionsManager(c *config.Sessions) *sessionsManager {
if c.DecodeCookie {
c.Cookie = base64.URLEncoding.EncodeToString([]byte(c.Cookie)) // change the cookie's name/key to a more safe(?)
// get the real value for your tests by:
//sessIdKey := url.QueryEscape(base64.URLEncoding.EncodeToString([]byte(iris.Config.Sessions.Cookie)))
}
manager := &sessionsManager{config: c, provider: &sessionProvider{list: list.New(), sessions: make(map[string]*list.Element, 0), databases: make([]SessionDatabase, 0), expires: c.Expires}}
//run the GC here
go manager.gc()
return manager
}
func (m *sessionsManager) registerDatabase(db SessionDatabase) {
m.provider.expires = m.config.Expires // updae the expires confiuration field for any case
m.provider.registerDatabase(db)
}
func (m *sessionsManager) generateSessionID() string {
return base64.URLEncoding.EncodeToString(utils.Random(32))
}
func domainCanPersistence(requestDomain string) bool {
if requestDomain == "0.0.0.0" || requestDomain == "127.0.0.1" {
// for these type of hosts, we can't allow subdomains persistance,
// the web browser doesn't understand the mysubdomain.0.0.0.0 and mysubdomain.127.0.0.1 mysubdomain.32.196.56.181. as scorrectly ubdomains because of the many dots
// so don't set a cookie domain here, let browser handle this
return false
}
dotLen := strings.Count(requestDomain, ".")
if dotLen == 0 {
// we don't have a domain, maybe something like 'localhost', browser doesn't see the .localhost as wildcard subdomain+domain
return false
}
if dotLen >= 3 {
if lastDotIdx := strings.LastIndexByte(requestDomain, '.'); lastDotIdx != -1 {
// chekc the last part, if it's number then propably it's ip
if len(requestDomain) > lastDotIdx+1 {
_, err := strconv.Atoi(requestDomain[lastDotIdx+1:])
if err == nil {
return false
}
}
}
}
return true
}
// Start starts the session
func (m *sessionsManager) start(ctx *Context) *session {
var session *session
cookieValue := ctx.GetCookie(m.config.Cookie)
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
sid := m.generateSessionID()
session = m.provider.init(sid)
cookie := fasthttp.AcquireCookie()
// 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.SetKey(m.config.Cookie)
cookie.SetValue(sid)
cookie.SetPath("/")
if !m.config.DisableSubdomainPersistence {
requestDomain := ctx.HostString()
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
if domainCanPersistence(requestDomain) {
// RFC2109, we allow level 1 subdomains, but no further
// if we have localhost.com , we want the localhost.com.
// so if we have something like: mysubdomain.localhost.com we want the localhost here
// if we have mysubsubdomain.mysubdomain.localhost.com we want the .mysubdomain.localhost.com here
// slow things here, especially the 'replace' but this is a good and understable( I hope) way to get the be able to set cookies from subdomains & domain with 1-level limit
if dotIdx := strings.LastIndexByte(requestDomain, '.'); dotIdx > 0 {
// is mysubdomain.localhost.com || mysubsubdomain.mysubdomain.localhost.com
s := requestDomain[0:dotIdx] // set mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost
if secondDotIdx := strings.LastIndexByte(s, '.'); secondDotIdx > 0 {
//is mysubdomain.localhost || mysubsubdomain.mysubdomain.localhost
s = s[secondDotIdx+1:] // set to localhost || mysubdomain.localhost
}
// replace the s with the requestDomain before the domain's siffux
subdomainSuff := strings.LastIndexByte(requestDomain, '.')
if subdomainSuff > len(s) { // if it is actual exists as subdomain suffix
requestDomain = strings.Replace(requestDomain, requestDomain[0:subdomainSuff], s, 1) // set to localhost.com || mysubdomain.localhost.com
}
}
// finally set the .localhost.com (for(1-level) || .mysubdomain.localhost.com (for 2-level subdomain allow)
cookie.SetDomain("." + requestDomain) // . to allow persistance
}
}
cookie.SetHTTPOnly(true)
if m.config.Expires == 0 {
// unlimited life
cookie.SetExpire(config.CookieExpireNever)
} else if m.config.Expires > 0 {
cookie.SetExpire(time.Now().Add(m.config.Expires))
} // if it's -1 then the cookie is deleted when the browser closes
ctx.SetCookie(cookie)
fasthttp.ReleaseCookie(cookie)
} else {
session = m.provider.read(cookieValue)
}
return session
}
// Destroy kills the session and remove the associated cookie
func (m *sessionsManager) destroy(ctx *Context) {
cookieValue := ctx.GetCookie(m.config.Cookie)
if cookieValue == "" { // nothing to destroy
return
}
ctx.RemoveCookie(m.config.Cookie)
m.provider.destroy(cookieValue)
}
// GC tick-tock for the store cleanup
// it's a blocking function, so run it with go routine, it's totally safe
func (m *sessionsManager) gc() {
m.provider.gc(m.config.GcDuration)
// set a timer for the next GC
time.AfterFunc(m.config.GcDuration, func() {
m.gc()
})
}