forked from alexedwards/scs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
session.go
456 lines (387 loc) · 10.8 KB
/
session.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
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
/*
Package session provides session management middleware and helpers for
manipulating session data.
It should be installed alongside one of the storage engines from https://godoc.org/github.com/alexedwards/scs/engine.
For example:
$ go get github.com/alexedwards/scs/session
$ go get github.com/alexedwards/scs/engine/memstore
Basic use:
package main
import (
"io"
"net/http"
"github.com/alexedwards/scs/engine/memstore"
"github.com/alexedwards/scs/session"
)
func main() {
// Initialise a new storage engine. Here we use the memstore package, but the principles
// are the same no matter which back-end store you choose.
engine := memstore.New(0)
// Initialise the session manager middleware, passing in the storage engine as
// the first parameter. This middleware will automatically handle loading and
// saving of session data for you.
sessionManager := session.Manage(engine)
// Set up your HTTP handlers in the normal way.
mux := http.NewServeMux()
mux.HandleFunc("/put", putHandler)
mux.HandleFunc("/get", getHandler)
// Wrap your handlers with the session manager middleware.
http.ListenAndServe(":4000", sessionManager(mux))
}
func putHandler(w http.ResponseWriter, r *http.Request) {
// Use the PutString helper to store a new key and associated string value in
// the session data. Helpers are also available for bool, int, int64, float,
// time.Time and []byte data types.
err := session.PutString(r, "message", "Hello from a session!")
if err != nil {
http.Error(w, err.Error(), 500)
}
}
func getHandler(w http.ResponseWriter, r *http.Request) {
// Use the GetString helper to retrieve the string value associated with a key.
msg, err := session.GetString(r, "message")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
io.WriteString(w, msg)
}
*/
package session
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"net/http"
"sync"
"time"
)
// ErrAlreadyWritten is returned when an attempt is made to modify the session
// data after it has already been sent to the storage engine and client.
var ErrAlreadyWritten = errors.New("session already written to the engine and http.ResponseWriter")
type session struct {
token string
data map[string]interface{}
deadline time.Time
engine Engine
opts *options
modified bool
written bool
mu sync.Mutex
}
func generateToken() (string, error) {
b := make([]byte, 32)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
func newSession(r *http.Request, engine Engine, opts *options) (*http.Request, error) {
token, err := generateToken()
if err != nil {
return nil, err
}
s := &session{
token: token,
data: make(map[string]interface{}),
deadline: time.Now().Add(opts.lifetime),
engine: engine,
opts: opts,
}
return requestWithSession(r, s), nil
}
func load(r *http.Request, engine Engine, opts *options) (*http.Request, error) {
cookie, err := r.Cookie(CookieName)
if err == http.ErrNoCookie {
return newSession(r, engine, opts)
} else if err != nil {
return nil, err
}
if cookie.Value == "" {
return newSession(r, engine, opts)
}
token := cookie.Value
j, found, err := engine.Find(token)
if err != nil {
return nil, err
}
if found == false {
return newSession(r, engine, opts)
}
data, deadline, err := decodeFromJSON(j)
if err != nil {
return nil, err
}
s := &session{
token: token,
data: data,
deadline: deadline,
engine: engine,
opts: opts,
}
return requestWithSession(r, s), nil
}
func write(w http.ResponseWriter, r *http.Request) error {
s, err := sessionFromContext(r)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.written == true {
return nil
}
if s.modified == false && s.opts.idleTimeout == 0 {
return nil
}
j, err := encodeToJSON(s.data, s.deadline)
if err != nil {
return err
}
expiry := s.deadline
if s.opts.idleTimeout > 0 {
ie := time.Now().Add(s.opts.idleTimeout)
if ie.Before(expiry) {
expiry = ie
}
}
if ce, ok := s.engine.(cookieEngine); ok {
s.token, err = ce.MakeToken(j, expiry)
if err != nil {
return err
}
} else {
err = s.engine.Save(s.token, j, expiry)
if err != nil {
return err
}
}
cookie := &http.Cookie{
Name: CookieName,
Value: s.token,
Path: s.opts.path,
Domain: s.opts.domain,
Secure: s.opts.secure,
HttpOnly: s.opts.httpOnly,
}
if s.opts.persist == true {
cookie.Expires = expiry
// The addition of 0.5 means MaxAge is correctly rounded to the nearest
// second instead of being floored.
cookie.MaxAge = int(expiry.Sub(time.Now()).Seconds() + 0.5)
}
http.SetCookie(w, cookie)
s.written = true
return nil
}
/*
RegenerateToken creates a new session token while retaining the current session
data. The session lifetime is also reset.
The old session token and accompanying data are deleted from the storage engine.
To mitigate the risk of session fixation attacks, it's important that you call
RegenerateToken before making any changes to privilege levels (e.g. login and
logout operations). See https://www.owasp.org/index.php/Session_fixation for
additional information.
Usage:
func loginHandler(w http.ResponseWriter, r *http.Request) {
userID := 123
// First regenerate the session token…
err := session.RegenerateToken(r)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
// Then make the privilege-level change.
err = session.PutInt(r, "userID", userID)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
}
*/
func RegenerateToken(r *http.Request) error {
s, err := sessionFromContext(r)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.written == true {
return ErrAlreadyWritten
}
err = s.engine.Delete(s.token)
if err != nil {
return err
}
token, err := generateToken()
if err != nil {
return err
}
s.token = token
s.deadline = time.Now().Add(s.opts.lifetime)
s.modified = true
return nil
}
// Renew creates a new session token and removes all data for the session. The
// session lifetime is also reset.
//
// The old session token and accompanying data are deleted from the storage engine.
//
// The Renew function is essentially a concurrency-safe amalgamation of the
// RegenerateToken and Clear functions.
func Renew(r *http.Request) error {
s, err := sessionFromContext(r)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.written == true {
return ErrAlreadyWritten
}
err = s.engine.Delete(s.token)
if err != nil {
return err
}
token, err := generateToken()
if err != nil {
return err
}
s.token = token
for key := range s.data {
delete(s.data, key)
}
s.deadline = time.Now().Add(s.opts.lifetime)
s.modified = true
return nil
}
// Destroy deletes the current session. The session token and accompanying
// data are deleted from the storage engine, and the client is instructed to
// delete the session cookie.
//
// Destroy operations are effective immediately, and any further operations on
// the session in the same request cycle will return an ErrAlreadyWritten error.
// If you see this error you probably want to use the Renew function instead.
//
// A new empty session will be created for any client that subsequently tries
// to use the destroyed session token.
func Destroy(w http.ResponseWriter, r *http.Request) error {
s, err := sessionFromContext(r)
if err != nil {
return err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.written == true {
return ErrAlreadyWritten
}
err = s.engine.Delete(s.token)
if err != nil {
return err
}
s.token = ""
for key := range s.data {
delete(s.data, key)
}
s.modified = true
cookie := &http.Cookie{
Name: CookieName,
Value: "",
Path: s.opts.path,
Domain: s.opts.domain,
Secure: s.opts.secure,
HttpOnly: s.opts.httpOnly,
Expires: time.Unix(1, 0),
MaxAge: -1,
}
http.SetCookie(w, cookie)
s.written = true
return nil
}
// Save immediately writes the session cookie header to the ResponseWriter and
// saves the session data to the storage engine, if needed.
//
// Using Save is not normally necessary. The session middleware (which buffers
// all writes to the underlying connection) will automatically handle setting the
// cookie header and storing the data for you.
//
// However there may be instances where you wish to break out of this normal
// operation and (one way or another) write to the underlying connection before
// control is passed back to the session middleware. In these instances, where
// response headers have already been written, the middleware will be too late
// to set the cookie header. The solution is to manually call Save before performing
// any writes.
//
// An example is flushing data using the http.Flusher interface:
//
// func flushingHandler(w http.ResponseWriter, r *http.Request) {
// err := session.PutString(r, "foo", "bar")
// if err != nil {
// http.Error(w, err.Error(), 500)
// return
// }
// err = session.Save(w, r)
// if err != nil {
// http.Error(w, err.Error(), 500)
// return
// }
//
// fw, ok := w.(http.Flusher)
// if !ok {
// http.Error(w, "could not assert to http.Flusher", 500)
// return
// }
// w.Write([]byte("This is some…"))
// fw.Flush()
// w.Write([]byte("flushed data"))
// }
func Save(w http.ResponseWriter, r *http.Request) error {
s, err := sessionFromContext(r)
if err != nil {
return err
}
s.mu.Lock()
wr := s.written
s.mu.Unlock()
if wr == true {
return ErrAlreadyWritten
}
return write(w, r)
}
func sessionFromContext(r *http.Request) (*session, error) {
s, ok := r.Context().Value(ContextName).(*session)
if ok == false {
return nil, errors.New("request.Context does not contain a *session value")
}
return s, nil
}
func requestWithSession(r *http.Request, s *session) *http.Request {
ctx := context.WithValue(r.Context(), ContextName, s)
return r.WithContext(ctx)
}
func encodeToJSON(data map[string]interface{}, deadline time.Time) ([]byte, error) {
return json.Marshal(&struct {
Data map[string]interface{} `json:"data"`
Deadline int64 `json:"deadline"`
}{
Data: data,
Deadline: deadline.UnixNano(),
})
}
func decodeFromJSON(j []byte) (map[string]interface{}, time.Time, error) {
aux := struct {
Data map[string]interface{} `json:"data"`
Deadline int64 `json:"deadline"`
}{}
dec := json.NewDecoder(bytes.NewReader(j))
dec.UseNumber()
err := dec.Decode(&aux)
if err != nil {
return nil, time.Time{}, err
}
return aux.Data, time.Unix(0, aux.Deadline), nil
}