forked from kataras/go-sessions
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathsessions.go
285 lines (255 loc) · 11.5 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
// Package sessions provides sessions support for net/http
// unique with auto-GC, register unlimited number of databases to Load and Update/Save the sessions in external server or to an external (no/or/and sql) database
// Usage net/http:
// // init a new sessions manager( if you use only one web framework inside your app then you can use the package-level functions like: sessions.Start/sessions.Destroy)
// manager := sessions.New(sessions.Config{})
// // start a session for a particular client
// manager.Start(http.ResponseWriter, *http.Request)
//
// // destroy a session from the server and client,
// // don't call it on each handler, only on the handler you want the client to 'logout' or something like this:
// manager.Destroy(http.ResponseWriter, *http.Request)
//
//
// Usage valyala/fasthttp:
// // init a new sessions manager( if you use only one web framework inside your app then you can use the package-level functions like: sessions.Start/sessions.Destroy)
// manager := sessions.New(sessions.Config{})
// // start a session for a particular client
// manager.StartFasthttp(*fasthttp.RequestCtx)
//
// // destroy a session from the server and client,
// // don't call it on each handler, only on the handler you want the client to 'logout' or something like this:
// manager.DestroyFasthttp(*fasthttp.Request)
//
// Note that, now, you can use both fasthttp and net/http within the same sessions manager(.New) instance!
// So now, you can share sessions between a net/http app and valyala/fasthttp app
package sessions
import (
"encoding/base64"
"github.com/valyala/fasthttp"
"net/http"
"strings"
"time"
)
const (
// Version current version number
Version = "0.0.3"
)
type (
// Sessions is the start point of this package
// contains all the registered sessions and manages them
Sessions interface {
// UpdateConfig updates the configuration field (Config does not receives a pointer, so this is a way to update a pre-defined configuration)
UpdateConfig(Config)
// UseDatabase ,optionally, adds a session database to the manager's provider,
// a session db doesn't have write access
// see https://github.com/kataras/go-sessions/tree/master/sessiondb
UseDatabase(Database)
// Start starts the session for the particular net/http request
Start(http.ResponseWriter, *http.Request) Session
// Destroy kills the net/http session and remove the associated cookie
Destroy(http.ResponseWriter, *http.Request)
// Start starts the session for the particular valyala/fasthttp request
StartFasthttp(*fasthttp.RequestCtx) Session
// Destroy kills the valyala/fasthttp session and remove the associated cookie
DestroyFasthttp(*fasthttp.RequestCtx)
}
// sessions contains the cookie's name, the provider and a duration for GC and cookie life expire
sessions struct {
config Config
provider *Provider
}
)
// New creates & returns a new Sessions(manager) and start its GC
func New(c Config) Sessions {
c = c.Validate()
// init and start the sess manager
sess := &sessions{config: c, provider: NewProvider(c.Expires)}
//run the GC here
go sess.gc()
return sess
}
var defaultSessions = New(Config{
Cookie: DefaultCookieName,
DecodeCookie: false,
Expires: DefaultCookieExpires,
CookieLength: DefaultCookieLength,
GcDuration: DefaultGcDuration,
DisableSubdomainPersistence: false,
})
// UpdateConfig updates the sessions configuration
func UpdateConfig(c Config) {
defaultSessions.UpdateConfig(c)
}
// UpdateConfig updates the sessions configuration
func (s *sessions) UpdateConfig(c Config) {
s.config = c.Validate()
}
// UseDatabase adds a session database to the manager's provider,
// a session db doesn't have write access
func UseDatabase(db Database) {
defaultSessions.UseDatabase(db)
}
// 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.Expires = s.config.Expires // updae the expires confiuration field for any case
s.provider.RegisterDatabase(db)
}
// Start starts the session for the particular net/http request
func Start(res http.ResponseWriter, req *http.Request) Session {
return defaultSessions.Start(res, req)
}
// Start starts the session for the particular net/http request
func (s *sessions) Start(res http.ResponseWriter, req *http.Request) Session {
var sess Session
cookieValue := GetCookie(s.config.Cookie, req)
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
sid := GenerateSessionID(s.config.CookieLength)
sess = s.provider.Init(sid)
//cookie := &http.Cookie{}
cookie := 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.Name = s.config.Cookie
cookie.Value = sid
cookie.Path = "/"
if !s.config.DisableSubdomainPersistence {
requestDomain := req.Host
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
if IsValidCookieDomain(requestDomain) {
// RFC2109, we allow level 1 subdomains, but no further
// if we have localhost.com , we want the localhost.cos.
// 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.Domain = "." + requestDomain // . to allow persistance
}
}
cookie.HttpOnly = true
if s.config.Expires == 0 {
// unlimited life
cookie.Expires = CookieExpireUnlimited
} else if s.config.Expires > 0 {
cookie.Expires = time.Now().Add(s.config.Expires)
} // if it's -1 then the cookie is deleted when the browser closes
AddCookie(cookie, res)
ReleaseCookie(cookie)
} else {
sess = s.provider.Read(cookieValue)
}
return sess
}
// Destroy kills the net/http session and remove the associated cookie
func Destroy(res http.ResponseWriter, req *http.Request) {
defaultSessions.Destroy(res, req)
}
// Destroy kills the net/http session and remove the associated cookie
func (s *sessions) Destroy(res http.ResponseWriter, req *http.Request) {
cookieValue := GetCookie(s.config.Cookie, req)
if cookieValue == "" { // nothing to destroy
return
}
RemoveCookie(s.config.Cookie, res, req)
s.provider.Destroy(cookieValue)
}
// StartFasthttp starts the session for the particular valyala/fasthttp request
func StartFasthttp(reqCtx *fasthttp.RequestCtx) Session {
return defaultSessions.StartFasthttp(reqCtx)
}
// Start starts the session for the particular valyala/fasthttp request
func (s *sessions) StartFasthttp(reqCtx *fasthttp.RequestCtx) Session {
var sess Session
cookieValue := GetFasthttpCookie(s.config.Cookie, reqCtx)
if cookieValue == "" { // cookie doesn't exists, let's generate a session and add set a cookie
sid := GenerateSessionID(s.config.CookieLength)
sess = s.provider.Init(sid)
cookie := fasthttp.AcquireCookie()
//cookie := &fasthttp.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.SetKey(s.config.Cookie)
cookie.SetValue(sid)
cookie.SetPath("/")
if !s.config.DisableSubdomainPersistence {
requestDomain := string(reqCtx.Host())
if portIdx := strings.IndexByte(requestDomain, ':'); portIdx > 0 {
requestDomain = requestDomain[0:portIdx]
}
if IsValidCookieDomain(requestDomain) {
// RFC2109, we allow level 1 subdomains, but no further
// if we have localhost.com , we want the localhost.cos.
// 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 s.config.Expires == 0 {
// unlimited life
cookie.SetExpire(CookieExpireUnlimited)
} else if s.config.Expires > 0 {
cookie.SetExpire(time.Now().Add(s.config.Expires))
} // if it's -1 then the cookie is deleted when the browser closes
AddFasthttpCookie(cookie, reqCtx)
fasthttp.ReleaseCookie(cookie)
} else {
sess = s.provider.Read(cookieValue)
}
return sess
}
// DestroyFasthttp kills the valyala/fasthttp session and remove the associated cookie
func DestroyFasthttp(reqCtx *fasthttp.RequestCtx) {
defaultSessions.DestroyFasthttp(reqCtx)
}
// DestroyFasthttp kills the valyala/fasthttp session and remove the associated cookie
func (s *sessions) DestroyFasthttp(reqCtx *fasthttp.RequestCtx) {
cookieValue := GetFasthttpCookie(s.config.Cookie, reqCtx)
if cookieValue == "" { // nothing to destroy
return
}
RemoveFasthttpCookie(s.config.Cookie, reqCtx)
s.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 (s *sessions) gc() {
s.provider.GC(s.config.GcDuration)
// set a timer for the next GC
time.AfterFunc(s.config.GcDuration, func() {
s.gc()
})
}
// GenerateSessionID returns a random string, used to set the session id
func GenerateSessionID(length int) string {
return base64.URLEncoding.EncodeToString(Random(length))
}