-
Notifications
You must be signed in to change notification settings - Fork 17
/
Copy pathstorage.go
121 lines (110 loc) · 3.12 KB
/
storage.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
package jeff
import (
"context"
"crypto/subtle"
"errors"
"time"
)
// Storage provides the base level abstraction for implementing session
// storage. Typically this would be memcache, redis or a database.
type Storage interface {
// Store persists the session in the backend with the given expiration
// Implementation must return value exactly as it is received.
// Value will be given as...
Store(ctx context.Context, key, value []byte, exp time.Time) error
// Fetch retrieves the session from the backend. If err != nil or
// value == nil, then it's assumed that the session is invalid and Jeff
// will redirect. Expired sessions must return nil error and nil value.
// Unknown (not found) sessions must return nil error and nil value.
Fetch(ctx context.Context, key []byte) (value []byte, err error)
// Delete removes the session given by key from the store. Errors are
// bubbled up to the caller. Delete should not return an error on expired
// or missing keys.
Delete(ctx context.Context, key []byte) error
}
func (j *Jeff) loadOne(ctx context.Context, key, tok []byte) (Session, error) {
l, err := j.load(ctx, key)
if err != nil {
return Session{}, err
}
s, i := find(l, tok)
if i < 0 {
return Session{}, errors.New("session not found")
}
return s, nil
}
func (j *Jeff) load(ctx context.Context, key []byte) (SessionList, error) {
stored, err := j.s.Fetch(ctx, key)
if err != nil || stored == nil {
return nil, err
}
var sl SessionList
_, err = sl.UnmarshalMsg(stored)
return sl, err
}
func find(l SessionList, k []byte) (Session, int) {
for i, s := range l {
if subtle.ConstantTimeCompare(s.Token, k) == 1 {
if s.Exp.Before(now()) {
break
}
return s, i
}
}
return Session{}, -1
}
func prune(l SessionList) SessionList {
ret := make(SessionList, 0, len(l))
for _, s := range l {
if s.Exp.Before(now()) {
continue
}
ret = append(ret, s)
}
return ret
}
func (j *Jeff) store(ctx context.Context, s Session) error {
sl, err := j.load(ctx, s.Key)
if err != nil {
return err
}
if _, i := find(sl, s.Token); i >= 0 {
sl[i] = s
} else {
sl = append(sl, s)
}
sl = prune(sl)
bts, err := sl.MarshalMsg(nil)
if err != nil {
return err
}
// Global Expiration 30d, TODO: make configurable
return j.s.Store(ctx, s.Key, bts, now().Add(24*30*time.Hour))
}
// Clear deletes all sessions for a given key, or it deletes the selected
// sessions if a list of tokens is given.
func (j *Jeff) clear(ctx context.Context, key []byte, tokens ...[]byte) error {
if len(tokens) == 0 {
return j.s.Delete(ctx, key)
}
sl, err := j.load(ctx, key)
if err != nil {
return err
}
// if it's found, remove it. This is O(N**2). Not sure what the best way
// to avoid this is. Might want to impose limits on the number of sessions
// per user and tokens passed into clear.
for _, tok := range tokens {
if _, i := find(sl, tok); i >= 0 {
sl = append(sl[:i], sl[i+1:]...)
}
}
// prune expired sessions
sl = prune(sl)
bts, err := sl.MarshalMsg(nil)
if err != nil {
return err
}
// Global Expiration 30d, TODO: make configurable
return j.s.Store(ctx, key, bts, now().Add(24*30*time.Hour))
}