forked from koajs/session
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontext.ts
More file actions
355 lines (314 loc) · 9.51 KB
/
Copy pathcontext.ts
File metadata and controls
355 lines (314 loc) · 9.51 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
import { debuglog } from 'node:util';
import { Session } from './session.js';
import util from './util.js';
import type { SessionOptions } from './index.js';
const debug = debuglog('koa-session:context');
const COOKIE_EXP_DATE = new Date(util.CookieDateEpoch);
const ONE_DAY = 24 * 60 * 60 * 1000;
export class ContextSession {
ctx: any;
app: any;
opts: SessionOptions;
store: SessionOptions['store'];
session: Session | false;
externalKey?: string;
prevHash?: number;
/**
* context session constructor
*/
constructor(ctx: any, opts: SessionOptions) {
this.ctx = ctx;
this.app = ctx.app;
this.opts = { ...opts };
this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;
}
/**
* internal logic of `ctx.session`
* @return {Session} session object
*/
get(): Session | null {
// already retrieved
if (this.session) return this.session;
// unset
if (this.session === false) return null;
// create an empty session or init from cookie
this.store ? this.create() : this.initFromCookie();
return this.session as Session;
}
/**
* internal logic of `ctx.session=`
* @param {Object} val session object
*/
set(val: Record<string, unknown> | null) {
if (val === null) {
this.session = false;
return;
}
if (typeof val === 'object') {
// use the original `externalKey` if exists to avoid waste storage
this.create(val, this.externalKey);
return;
}
throw new Error('this.session can only be set as null or an object.');
}
/**
* init session from external store
* will be called in the front of session middleware
*
* @public
*/
async initFromExternal() {
debug('init from external');
const ctx = this.ctx;
const opts = this.opts;
let externalKey;
if (opts.externalKey) {
externalKey = opts.externalKey.get(ctx);
debug('get external key from custom %s', externalKey);
} else {
externalKey = ctx.cookies.get(opts.key, opts);
debug('get external key from cookie %s', externalKey);
}
if (!externalKey) {
// create a new `externalKey`
this.create();
return;
}
const sessionData = await this.store!.get(externalKey, opts.maxAge as number, { ctx, rolling: opts.rolling });
if (!this.valid(sessionData, externalKey)) {
debug('invalid session data, create a new session');
// create a new `externalKey`
this.create();
return;
}
// create with original `externalKey`
this.create(sessionData, externalKey);
this.prevHash = util.hash((this.session as Session).toJSON());
}
/**
* init session from cookie
* @private
*/
initFromCookie() {
debug('init from cookie');
const ctx = this.ctx;
const opts = this.opts;
const cookie = ctx.cookies.get(opts.key, opts);
if (!cookie) {
this.create();
return;
}
let sessionData: Record<string, unknown>;
debug('parse cookie: %j', cookie);
try {
sessionData = opts.decode(cookie);
} catch (err: unknown) {
// backwards compatibility:
// create a new session if parsing fails.
// `Buffer.from(string, 'base64')` does not seem to crash
// when `string` is not base64-encoded.
// but `JSON.parse(string)` will crash.
debug('decode %j error: %s', cookie, err);
if (err instanceof Error && !(err instanceof SyntaxError)) {
// clean this cookie to ensure next request won't throw again
ctx.cookies.set(opts.key, '', opts);
// `ctx.onerror` will unset all headers, and set those specified in err
Reflect.set(err, 'headers', {
'set-cookie': ctx.response.get('set-cookie'),
});
throw err;
}
this.create();
return;
}
debug('parsed session data: %j', sessionData);
if (!this.valid(sessionData)) {
// create a new session if the session data is invalid
this.create();
debug('invalid session data, create a new session');
return;
}
// support access `ctx.session` before session middleware
this.create(sessionData);
this.prevHash = util.hash((this.session as Session).toJSON());
}
/**
* verify session(expired or custom verification)
* @param {Object} sessionData session data
* @param {Object} [key] session externalKey(optional)
* @private
*/
protected valid(sessionData: Record<string, unknown>, key?: string) {
const ctx = this.ctx;
if (!sessionData) {
this.emit('missed', { key, value: sessionData, ctx });
return false;
}
if (typeof sessionData._expire === 'number' && sessionData._expire < Date.now()) {
debug('expired session');
this.emit('expired', { key, value: sessionData, ctx });
return false;
}
const valid = this.opts.valid;
if (typeof valid === 'function' && !valid(ctx, sessionData)) {
// valid session value fail, ignore this session
debug('invalid session');
this.emit('invalid', { key, value: sessionData, ctx });
return false;
}
return true;
}
/**
* @param {String} event event name
* @param {Object} data event data
* @private
*/
emit(event: string, data: unknown) {
setImmediate(() => {
this.app.emit(`session:${event}`, data);
});
}
/**
* create a new session and attach to ctx.sess
*
* @param {Object} [sessionData] session data
* @param {String} [externalKey] session external key
*/
protected create(sessionData?: Record<string, unknown>, externalKey?: string) {
debug('create session with data: %j, externalKey: %s', sessionData, externalKey);
if (this.store) {
this.externalKey = externalKey ?? this.opts.genid?.(this.ctx);
}
this.session = new Session(this, sessionData, this.externalKey);
}
/**
* Commit the session changes or removal.
*/
async commit({ save = false, regenerate = false } = {}) {
const session = this.session;
const opts = this.opts;
const ctx = this.ctx;
// not accessed
if (session === undefined) {
return;
}
// removed
if (session === false) {
await this.remove();
return;
}
if (regenerate) {
await this.remove();
if (this.store) {
this.externalKey = opts.genid?.(ctx);
}
}
// force save session when `session._requireSave` set
const reason = save || regenerate || session._requireSave ? 'force' : this._shouldSaveSession();
debug('should save session: %j', reason);
if (!reason) {
return;
}
if (typeof opts.beforeSave === 'function') {
debug('before save');
opts.beforeSave(ctx, session);
}
const changed = reason === 'changed';
await this.save(changed);
}
_shouldSaveSession() {
const prevHash = this.prevHash;
const session = this.session as Session;
// do nothing if new and not populated
const sessionData = session.toJSON();
if (!prevHash && !Object.keys(sessionData).length) {
return '';
}
// save if session changed
const changed = prevHash !== util.hash(sessionData);
if (changed) {
return 'changed';
}
// save if opts.rolling set
if (this.opts.rolling) {
return 'rolling';
}
// save if opts.renew and session will expired
if (this.opts.renew) {
const expire = session._expire;
const maxAge = session.maxAge;
// renew when session will expired in maxAge / 2
if (expire && maxAge && expire - Date.now() < maxAge / 2) {
return 'renew';
}
}
// don't save
return '';
}
/**
* remove session
* @private
*/
async remove() {
// Override the default options so that we can properly expire the session cookies
const opts = {
...this.opts,
expires: COOKIE_EXP_DATE,
maxAge: false,
};
const ctx = this.ctx;
const key = opts.key;
const externalKey = this.externalKey;
if (externalKey) {
await this.store!.destroy(externalKey, { ctx });
}
ctx.cookies.set(key, '', opts);
}
/**
* save session
* @private
*/
async save(changed: boolean) {
const opts = this.opts;
const key = opts.key;
const externalKey = this.externalKey;
const sessionData = (this.session as Session).toJSON();
// set expire for check
let maxAge = opts.maxAge ? opts.maxAge : ONE_DAY;
if (maxAge === 'session') {
// do not set _expire in json if maxAge is set to 'session'
// also delete maxAge from options
opts.maxAge = undefined;
sessionData._session = true;
} else {
// set expire for check
sessionData._expire = maxAge + Date.now();
sessionData._maxAge = maxAge;
}
// save to external store
if (externalKey) {
debug('save %j to external key %s', sessionData, externalKey);
if (typeof maxAge === 'number') {
// ensure store expired after cookie
maxAge += 10000;
}
await this.store!.set(externalKey, sessionData, maxAge as number, {
changed,
ctx: this.ctx,
rolling: opts.rolling,
});
if (opts.externalKey) {
opts.externalKey.set(this.ctx, externalKey);
} else {
this.ctx.cookies.set(key, externalKey, opts);
}
return;
}
// save to cookie with base64 encode string
debug('save session data %j to cookie', sessionData);
const base64String = opts.encode(sessionData);
debug('save session data json base64 format: %s to cookie key: %s with options: %j',
base64String, key, opts);
this.ctx.cookies.set(key, base64String, opts);
}
}