-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathstore.go
More file actions
382 lines (341 loc) · 10.2 KB
/
Copy pathstore.go
File metadata and controls
382 lines (341 loc) · 10.2 KB
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
package session
import (
"context"
"encoding/gob"
"errors"
"fmt"
"time"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
"github.com/gofiber/fiber/v3/internal/storage/memory"
"github.com/gofiber/fiber/v3/log"
)
// ErrEmptySessionID is an error that occurs when the session ID is empty.
var (
ErrEmptySessionID = errors.New("session ID cannot be empty")
ErrSessionAlreadyLoadedByMiddleware = errors.New("session already loaded by middleware")
ErrSessionIDNotFoundInStore = errors.New("session ID not found in session store")
)
// sessionIDKey is the local key type used to store and retrieve the session ID in context.
type sessionIDKey int
const (
// sessionIDContextKey is the key used to store the session ID in the context locals.
sessionIDContextKey sessionIDKey = iota
)
// sessionIDInfo bundles the resolved session ID with the extractor source that
// produced it. Both pieces are cached together in the request locals so that a
// second Store.Get within the same request returns a consistent answer. In
// particular, chained extractors keep their original source decision instead of
// being re-derived from the wrapper Extractor.Source.
type sessionIDInfo struct {
id string
source extractors.Source
}
// Store manages session data using the configured storage backend.
type Store struct {
Config
}
// NewStore creates a new session store with the provided configuration.
//
// Parameters:
// - config: Variadic parameter to override default config.
//
// Returns:
// - *Store: The session store.
//
// Usage:
//
// store := session.NewStore()
func NewStore(config ...Config) *Store {
// Set default config
cfg := configDefault(config...)
if cfg.Storage == nil {
cfg.Storage = memory.New()
}
store := &Store{
Config: cfg,
}
if cfg.AbsoluteTimeout > 0 {
store.RegisterType(absExpirationKey)
store.RegisterType(time.Time{})
}
return store
}
// RegisterType registers a custom type for encoding/decoding into any storage provider.
//
// Parameters:
// - i: The custom type to register.
//
// Usage:
//
// store.RegisterType(MyCustomType{})
func (*Store) RegisterType(i any) {
gob.Register(i)
}
// Get will get/create a session.
//
// This function will return an ErrSessionAlreadyLoadedByMiddleware if
// the session is already loaded by the middleware.
//
// Parameters:
// - c: The Fiber context.
//
// Returns:
// - *Session: The session object.
// - error: An error if the session retrieval fails or if the session is already loaded by the middleware.
//
// Usage:
//
// sess, err := store.Get(c)
// if err != nil {
// // handle error
// }
func (s *Store) Get(c fiber.Ctx) (*Session, error) {
// If session is already loaded in the context,
// it should not be loaded again
_, ok := c.Locals(middlewareContextKey).(*Middleware)
if ok {
return nil, ErrSessionAlreadyLoadedByMiddleware
}
return s.getSession(c)
}
// getSession retrieves a session based on the context.
//
// Parameters:
// - c: The Fiber context.
//
// Returns:
// - *Session: The session object.
// - error: An error if the session retrieval fails.
//
// Usage:
//
// sess, err := store.getSession(c)
// if err != nil {
// // handle error
// }
func (s *Store) getSession(c fiber.Ctx) (*Session, error) {
var rawData []byte
var err error
// Resolve the session ID and the source that produced it. The pair is cached
// in the request locals so a second call within the same request returns the
// same answer, including for chained extractors where the source is decided
// at extraction time and would otherwise be lost.
info, alreadyResolved := c.Locals(sessionIDContextKey).(sessionIDInfo)
if !alreadyResolved {
info = s.resolveSessionID(c)
c.Locals(sessionIDContextKey, info)
}
id := info.id
fresh := false // Session is not fresh initially; only set to true if we generate a new ID
// Attempt to fetch session data if an ID is provided
if id != "" {
rawData, err = s.Storage.GetWithContext(c, id)
if err != nil {
return nil, err
}
if rawData == nil {
switch {
case alreadyResolved:
// A prior call within this request already committed to this ID.
// Keep it so multiple Store.Get calls in the same request observe
// the same session.
fresh = true
case s.acceptClientID(info):
// Read-only source with an opt-in trusted client ID; preserve so
// that subsequent requests carrying the same ID load the same
// session.
fresh = true
default:
// Writable source (cookie/header) with an unknown ID, or
// untrusted read-only ID; discard and generate a fresh one to
// prevent session fixation and storage poisoning.
id = ""
}
}
}
// Generate a new ID if needed
if id == "" {
fresh = true // The session is fresh if a new ID is generated
id = s.KeyGenerator()
// Mark the cached source as cookie so the regenerated ID is treated as
// server-issued (writable) on any subsequent call within this request.
c.Locals(sessionIDContextKey, sessionIDInfo{id: id, source: extractors.SourceCookie})
}
// Create session object
sess := acquireSession()
sess.mu.Lock()
sess.ctx = c
sess.config = s
sess.id = id
sess.fresh = fresh
// Decode session data if found
if rawData != nil {
sess.data.Lock()
err := sess.decodeSessionData(rawData)
sess.data.Unlock()
if err != nil {
sess.mu.Unlock()
sess.Release()
return nil, fmt.Errorf("failed to decode session data: %w", err)
}
}
sess.mu.Unlock()
if fresh && s.AbsoluteTimeout > 0 {
sess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout))
} else if sess.isAbsExpired() {
if err := sess.Reset(); err != nil {
return nil, fmt.Errorf("failed to reset session: %w", err)
}
sess.setAbsExpiration(time.Now().Add(s.AbsoluteTimeout))
}
return sess, nil
}
// resolveSessionID extracts the session ID from the request and reports the
// source that produced it. For chained extractors the sub-extractors are tried
// in order so the source of the first one that yields a value wins; for a
// single extractor the source on the wrapper is used. When extraction fails the
// returned ID is empty and the source falls back to the wrapper's source.
//
// Parameters:
// - c: The Fiber context.
//
// Returns:
// - sessionIDInfo: The resolved ID together with its originating source.
func (s *Store) resolveSessionID(c fiber.Ctx) sessionIDInfo {
ext := s.Extractor
if len(ext.Chain) > 0 {
for _, chainExt := range ext.Chain {
if chainExt.Extract == nil {
continue
}
v, err := chainExt.Extract(c)
if err == nil && v != "" {
return sessionIDInfo{id: v, source: chainExt.Source}
}
}
return sessionIDInfo{source: ext.Source}
}
v, err := ext.Extract(c)
if err != nil {
return sessionIDInfo{source: ext.Source}
}
return sessionIDInfo{id: v, source: ext.Source}
}
// acceptClientID reports whether a client-supplied session ID from a read-only
// source should be persisted as-is. Writable sources (cookie/header) are never
// accepted here; they are subject to fixation protection. For read-only
// sources the application must explicitly opt in via TrustClientSessionID and
// supply a ClientSessionIDValidator that accepts the ID; otherwise the ID is
// rejected and a server-generated one is used.
func (s *Store) acceptClientID(info sessionIDInfo) bool {
if info.id == "" || info.source.IsWritable() {
return false
}
if !s.TrustClientSessionID || s.ClientSessionIDValidator == nil {
return false
}
return s.ClientSessionIDValidator(info.id)
}
// Reset deletes all sessions from the storage.
//
// Returns:
// - error: An error if the reset operation fails.
//
// Usage:
//
// err := store.Reset()
// if err != nil {
// // handle error
// }
func (s *Store) Reset(ctx context.Context) error {
return s.Storage.ResetWithContext(ctx)
}
// Delete deletes a session by its ID.
//
// Parameters:
// - id: The unique identifier of the session.
//
// Returns:
// - error: An error if the deletion fails or if the session ID is empty.
//
// Usage:
//
// err := store.Delete(id)
// if err != nil {
// // handle error
// }
func (s *Store) Delete(ctx context.Context, id string) error {
if id == "" {
return ErrEmptySessionID
}
return s.Storage.DeleteWithContext(ctx, id)
}
// GetByID retrieves a session by its ID from the storage.
// If the session is not found, it returns nil and an error.
//
// Unlike session middleware methods, this function does not automatically:
//
// - Load the session into the request context.
//
// - Save the session data to the storage or update the client cookie.
//
// Important Notes:
//
// - The session object returned by GetByID does not have a context associated with it.
//
// - When using this method alongside session middleware, there is a potential for collisions,
// so be mindful of interactions between manually retrieved sessions and middleware-managed sessions.
//
// - If you modify a session returned by GetByID, you must call session.Save() to persist the changes.
//
// - When you are done with the session, you should call session.Release() to release the session back to the pool.
//
// Parameters:
// - id: The unique identifier of the session.
//
// Returns:
// - *Session: The session object if found; otherwise, nil.
// - error: An error if the session retrieval fails or if the session ID is empty.
//
// Usage:
//
// sess, err := store.GetByID(id)
// if err != nil {
// // handle error
// }
func (s *Store) GetByID(ctx context.Context, id string) (*Session, error) {
if id == "" {
return nil, ErrEmptySessionID
}
rawData, err := s.Storage.GetWithContext(ctx, id)
if err != nil {
return nil, err
}
if rawData == nil {
return nil, ErrSessionIDNotFoundInStore
}
sess := acquireSession()
sess.mu.Lock()
sess.config = s
sess.id = id
sess.fresh = false
sess.data.Lock()
decodeErr := sess.decodeSessionData(rawData)
sess.data.Unlock()
sess.mu.Unlock()
if decodeErr != nil {
sess.Release()
return nil, fmt.Errorf("failed to decode session data: %w", decodeErr)
}
if s.AbsoluteTimeout > 0 {
if sess.isAbsExpired() {
if err := sess.Destroy(); err != nil { //nolint:contextcheck // it is not right
sess.Release()
log.Errorf("failed to destroy session: %v", err)
}
return nil, ErrSessionIDNotFoundInStore
}
}
return sess, nil
}