-
Notifications
You must be signed in to change notification settings - Fork 281
/
cookie_store.go
225 lines (200 loc) · 5.61 KB
/
cookie_store.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
// Package cookie provides a cookie based implementation of session store and loader.
package cookie
import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/pomerium/pomerium/internal/encoding"
"github.com/pomerium/pomerium/internal/sessions"
)
var _ sessions.SessionStore = &Store{}
var _ sessions.SessionLoader = &Store{}
// timeNow is time.Now but pulled out as a variable for tests.
var timeNow = time.Now
const (
// ChunkedCanaryByte is the byte value used as a canary prefix to distinguish if
// the cookie is multi-part or not. This constant *should not* be valid
// base64. It's important this byte is ASCII to avoid UTF-8 variable sized runes.
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#Directives
ChunkedCanaryByte byte = '%'
// MaxChunkSize sets the upper bound on a cookie chunks payload value.
// Note, this should be lower than the actual cookie's max size (4096 bytes)
// which includes metadata.
MaxChunkSize = 3800
// MaxNumChunks limits the number of chunks to iterate through. Conservatively
// set to prevent any abuse.
MaxNumChunks = 5
)
// Store implements the session store interface for session cookies.
type Store struct {
Name string
Domain string
Expire time.Duration
HTTPOnly bool
Secure bool
encoder encoding.Marshaler
decoder encoding.Unmarshaler
}
// Options holds options for Store
type Options struct {
Name string
Domain string
Expire time.Duration
HTTPOnly bool
Secure bool
}
// NewStore returns a new store that implements the SessionStore interface
// using http cookies.
func NewStore(opts *Options, encoder encoding.MarshalUnmarshaler) (sessions.SessionStore, error) {
cs, err := NewCookieLoader(opts, encoder)
if err != nil {
return nil, err
}
cs.encoder = encoder
return cs, nil
}
// NewCookieLoader returns a new store that implements the SessionLoader
// interface using http cookies.
func NewCookieLoader(opts *Options, dencoder encoding.Unmarshaler) (*Store, error) {
if dencoder == nil {
return nil, fmt.Errorf("internal/sessions: dencoder cannot be nil")
}
cs, err := newStore(opts)
if err != nil {
return nil, err
}
cs.decoder = dencoder
return cs, nil
}
func newStore(opts *Options) (*Store, error) {
if opts.Name == "" {
return nil, fmt.Errorf("internal/sessions: cookie name cannot be empty")
}
return &Store{
Name: opts.Name,
Secure: opts.Secure,
HTTPOnly: opts.HTTPOnly,
Domain: opts.Domain,
Expire: opts.Expire,
}, nil
}
func (cs *Store) makeCookie(value string) *http.Cookie {
return &http.Cookie{
Name: cs.Name,
Value: value,
Path: "/",
Domain: cs.Domain,
HttpOnly: cs.HTTPOnly,
Secure: cs.Secure,
Expires: timeNow().Add(cs.Expire),
}
}
// ClearSession clears the session cookie from a request
func (cs *Store) ClearSession(w http.ResponseWriter, r *http.Request) {
c := cs.makeCookie("")
c.MaxAge = -1
c.Expires = timeNow().Add(-time.Hour)
http.SetCookie(w, c)
}
func getCookies(r *http.Request, name string) []*http.Cookie {
allCookies := r.Cookies()
matchedCookies := make([]*http.Cookie, 0, len(allCookies))
for _, c := range allCookies {
if strings.EqualFold(c.Name, name) {
matchedCookies = append(matchedCookies, c)
}
}
return matchedCookies
}
// LoadSession returns a State from the cookie in the request.
func (cs *Store) LoadSession(r *http.Request) (string, error) {
cookies := getCookies(r, cs.Name)
if len(cookies) == 0 {
return "", sessions.ErrNoSessionFound
}
for _, cookie := range cookies {
jwt := loadChunkedCookie(r, cookie)
session := &sessions.State{}
err := cs.decoder.Unmarshal([]byte(jwt), session)
if err == nil {
return jwt, nil
}
}
return "", sessions.ErrMalformed
}
// SaveSession saves a session state to a request's cookie store.
func (cs *Store) SaveSession(w http.ResponseWriter, _ *http.Request, x interface{}) error {
var value string
switch v := x.(type) {
case []byte:
value = string(v)
case string:
value = v
default:
if cs.encoder == nil {
return errors.New("internal/sessions: cannot save non-string type")
}
data, err := cs.encoder.Marshal(x)
if err != nil {
return err
}
value = string(data)
}
cs.setSessionCookie(w, value)
return nil
}
func (cs *Store) setSessionCookie(w http.ResponseWriter, val string) {
cs.setCookie(w, cs.makeCookie(val))
}
func (cs *Store) setCookie(w http.ResponseWriter, cookie *http.Cookie) {
if len(cookie.String()) <= MaxChunkSize {
http.SetCookie(w, cookie)
return
}
for i, c := range chunk(cookie.Value, MaxChunkSize) {
// start with a copy of our original cookie
nc := *cookie
if i == 0 {
// if this is the first cookie, add our canary byte
nc.Value = fmt.Sprintf("%s%s", string(ChunkedCanaryByte), c)
} else {
// subsequent parts will be postfixed with their part number
nc.Name = fmt.Sprintf("%s_%d", cookie.Name, i)
nc.Value = c
}
http.SetCookie(w, &nc)
}
}
func loadChunkedCookie(r *http.Request, c *http.Cookie) string {
if len(c.Value) == 0 {
return ""
}
// if the first byte is our canary byte, we need to handle the multipart bit
if []byte(c.Value)[0] != ChunkedCanaryByte {
return c.Value
}
data := c.Value
var b strings.Builder
fmt.Fprintf(&b, "%s", data[1:])
for i := 1; i <= MaxNumChunks; i++ {
next, err := r.Cookie(fmt.Sprintf("%s_%d", c.Name, i))
if err != nil {
break // break if we can't find the next cookie
}
fmt.Fprintf(&b, "%s", next.Value)
}
data = b.String()
return data
}
func chunk(s string, size int) []string {
ss := make([]string, 0, len(s)/size+1)
for len(s) > 0 {
if len(s) < size {
size = len(s)
}
ss, s = append(ss, s[:size]), s[size:]
}
return ss
}