UNPKG

7.13 kBJavaScriptView Raw
1'use strict';
2
3const debug = require('debug')('koa-session:context');
4const Session = require('./session');
5const util = require('./util');
6
7const ONE_DAY = 24 * 60 * 60 * 1000;
8
9class ContextSession {
10 /**
11 * context session constructor
12 * @api public
13 */
14
15 constructor(ctx, opts) {
16 this.ctx = ctx;
17 this.app = ctx.app;
18 this.opts = Object.assign({}, opts);
19 this.store = this.opts.ContextStore ? new this.opts.ContextStore(ctx) : this.opts.store;
20 }
21
22 /**
23 * internal logic of `ctx.session`
24 * @return {Session} session object
25 *
26 * @api public
27 */
28
29 get() {
30 const session = this.session;
31 // already retrieved
32 if (session) return session;
33 // unset
34 if (session === false) return null;
35
36 // cookie session store
37 if (!this.store) this.initFromCookie();
38 return this.session;
39 }
40
41 /**
42 * internal logic of `ctx.session=`
43 * @param {Object} val session object
44 *
45 * @api public
46 */
47
48 set(val) {
49 if (val === null) {
50 this.session = false;
51 return;
52 }
53 if (typeof val === 'object') {
54 // use the original `externalKey` if exists to avoid waste storage
55 this.create(val, this.externalKey);
56 return;
57 }
58 throw new Error('this.session can only be set as null or an object.');
59 }
60
61 /**
62 * init session from external store
63 * will be called in the front of session middleware
64 *
65 * @api public
66 */
67
68 * initFromExternal() {
69 debug('init from external');
70 const ctx = this.ctx;
71 const opts = this.opts;
72
73 const externalKey = ctx.cookies.get(opts.key, opts);
74 debug('get external key from cookie %s', externalKey);
75
76 if (!externalKey) {
77 // create a new `externalKey`
78 this.create();
79 return;
80 }
81
82 const json = yield this.store.get(externalKey, opts.maxAge, { rolling: opts.rolling });
83 if (!this.valid(json, externalKey)) {
84 // create a new `externalKey`
85 this.create();
86 return;
87 }
88
89 // create with original `externalKey`
90 this.create(json, externalKey);
91 this.prevHash = util.hash(this.session.toJSON());
92 }
93
94 /**
95 * init session from cookie
96 * @api private
97 */
98
99 initFromCookie() {
100 debug('init from cookie');
101 const ctx = this.ctx;
102 const opts = this.opts;
103
104 const cookie = ctx.cookies.get(opts.key, opts);
105 if (!cookie) {
106 this.create();
107 return;
108 }
109
110 let json;
111 debug('parse %s', cookie);
112 try {
113 json = opts.decode(cookie);
114 } catch (err) {
115 // backwards compatibility:
116 // create a new session if parsing fails.
117 // new Buffer(string, 'base64') does not seem to crash
118 // when `string` is not base64-encoded.
119 // but `JSON.parse(string)` will crash.
120 debug('decode %j error: %s', cookie, err);
121 if (!(err instanceof SyntaxError)) {
122 // clean this cookie to ensure next request won't throw again
123 ctx.cookies.set(opts.key, '', opts);
124 // ctx.onerror will unset all headers, and set those specified in err
125 err.headers = {
126 'set-cookie': ctx.response.get('set-cookie'),
127 };
128 throw err;
129 }
130 this.create();
131 return;
132 }
133
134 debug('parsed %j', json);
135
136 if (!this.valid(json)) {
137 this.create();
138 return;
139 }
140
141 // support access `ctx.session` before session middleware
142 this.create(json);
143 this.prevHash = util.hash(this.session.toJSON());
144 }
145
146 /**
147 * verify session(expired or )
148 * @param {Object} value session object
149 * @param {Object} key session externalKey(optional)
150 * @return {Boolean} valid
151 * @api private
152 */
153
154 valid(value, key) {
155 const ctx = this.ctx;
156 if (!value) {
157 this.emit('missed', { key, value, ctx });
158 return false;
159 }
160
161 if (value._expire && value._expire < Date.now()) {
162 debug('expired session');
163 this.emit('expired', { key, value, ctx });
164 return false;
165 }
166
167 const valid = this.opts.valid;
168 if (typeof valid === 'function' && !valid(ctx, value)) {
169 // valid session value fail, ignore this session
170 debug('invalid session');
171 this.emit('invalid', { key, value, ctx });
172 return false;
173 }
174 return true;
175 }
176
177 /**
178 * @param {String} event event name
179 * @param {Object} data event data
180 * @api private
181 */
182 emit(event, data) {
183 setImmediate(() => {
184 this.app.emit(`session:${event}`, data);
185 });
186 }
187
188 /**
189 * create a new session and attach to ctx.sess
190 *
191 * @param {Object} [val] session data
192 * @param {String} [externalKey] session external key
193 * @api private
194 */
195
196 create(val, externalKey) {
197 debug('create session with val: %j externalKey: %s', val, externalKey);
198 if (this.store) this.externalKey = externalKey || this.opts.genid();
199 this.session = new Session(this.ctx, val);
200 }
201
202 /**
203 * Commit the session changes or removal.
204 *
205 * @api public
206 */
207
208 * commit() {
209 const session = this.session;
210 const prevHash = this.prevHash;
211 const opts = this.opts;
212 const ctx = this.ctx;
213
214 // not accessed
215 if (undefined === session) return;
216
217 // removed
218 if (session === false) {
219 yield this.remove();
220 return;
221 }
222
223 // force save session when `session._requireSave` set
224 let changed = true;
225 if (!session._requireSave) {
226 const json = session.toJSON();
227 // do nothing if new and not populated
228 if (!prevHash && !Object.keys(json).length) return;
229 changed = prevHash !== util.hash(json);
230 // do nothing if not changed and not in rolling mode
231 if (!this.opts.rolling && !changed) return;
232 }
233
234 if (typeof opts.beforeSave === 'function') {
235 debug('before save');
236 opts.beforeSave(ctx, session);
237 }
238 yield this.save(changed);
239 }
240
241 /**
242 * remove session
243 * @api private
244 */
245
246 * remove() {
247 const opts = this.opts;
248 const ctx = this.ctx;
249 const key = opts.key;
250 const externalKey = this.externalKey;
251
252 if (externalKey) yield this.store.destroy(externalKey);
253 ctx.cookies.set(key, '', opts);
254 }
255
256 /**
257 * save session
258 * @api private
259 */
260
261 * save(changed) {
262 const opts = this.opts;
263 const key = opts.key;
264 const externalKey = this.externalKey;
265 let json = this.session.toJSON();
266 // set expire for check
267 const maxAge = opts.maxAge ? opts.maxAge : ONE_DAY;
268 if (maxAge === 'session') {
269 // do not set _expire in json if maxAge is set to 'session'
270 // also delete maxAge from options
271 opts.maxAge = undefined;
272 } else {
273 // set expire for check
274 json._expire = maxAge + Date.now();
275 json._maxAge = maxAge;
276 }
277
278 // save to external store
279 if (externalKey) {
280 debug('save %j to external key %s', json, externalKey);
281 yield this.store.set(externalKey, json, maxAge, {
282 changed,
283 rolling: opts.rolling,
284 });
285 this.ctx.cookies.set(key, externalKey, opts);
286 return;
287 }
288
289 // save to cookie
290 debug('save %j to cookie', json);
291 json = opts.encode(json);
292 debug('save %s', json);
293
294 this.ctx.cookies.set(key, json, opts);
295 }
296}
297
298module.exports = ContextSession;