UNPKG

5.29 kBJavaScriptView Raw
1/*!
2 * cookie-session
3 * Copyright(c) 2013 Jonathan Ong
4 * Copyright(c) 2014-2015 Douglas Christopher Wilson
5 * MIT Licensed
6 */
7
8'use strict'
9
10/**
11 * Module dependencies.
12 * @private
13 */
14
15var debug = require('debug')('cookie-session');
16var Cookies = require('cookies');
17var onHeaders = require('on-headers');
18
19/**
20 * Module exports.
21 * @public
22 */
23
24module.exports = cookieSession
25
26/**
27 * Create a new cookie session middleware.
28 *
29 * @param {object} [options]
30 * @param {boolean} [options.httpOnly=true]
31 * @param {array} [options.keys]
32 * @param {string} [options.name=express:sess] Name of the cookie to use
33 * @param {boolean} [options.overwrite=true]
34 * @param {string} [options.secret]
35 * @param {boolean} [options.signed=true]
36 * @return {function} middleware
37 * @public
38 */
39
40function cookieSession(options) {
41 var opts = options || {}
42
43 // name - previously "opts.key"
44 var name = opts.name || opts.key || 'express:sess';
45
46 // secrets
47 var keys = opts.keys;
48 if (!keys && opts.secret) keys = [opts.secret];
49
50 // defaults
51 if (null == opts.overwrite) opts.overwrite = true;
52 if (null == opts.httpOnly) opts.httpOnly = true;
53 if (null == opts.signed) opts.signed = true;
54
55 if (!keys && opts.signed) throw new Error('.keys required.');
56
57 debug('session options %j', opts);
58
59 return function _cookieSession(req, res, next) {
60 var cookies = req.sessionCookies = new Cookies(req, res, {
61 keys: keys
62 });
63 var sess, json;
64
65 // to pass to Session()
66 req.sessionOptions = Object.create(opts)
67 req.sessionKey = name
68
69 req.__defineGetter__('session', function(){
70 // already retrieved
71 if (sess) return sess;
72
73 // unset
74 if (false === sess) return null;
75
76 json = cookies.get(name, req.sessionOptions)
77
78 if (json) {
79 debug('parse %s', json);
80 try {
81 sess = new Session(req, decode(json));
82 } catch (err) {
83 // backwards compatibility:
84 // create a new session if parsing fails.
85 // new Buffer(string, 'base64') does not seem to crash
86 // when `string` is not base64-encoded.
87 // but `JSON.parse(string)` will crash.
88 if (!(err instanceof SyntaxError)) throw err;
89 sess = new Session(req);
90 }
91 } else {
92 debug('new session');
93 sess = new Session(req);
94 }
95
96 return sess;
97 });
98
99 req.__defineSetter__('session', function(val){
100 if (null == val) return sess = false;
101 if ('object' == typeof val) return sess = new Session(req, val);
102 throw new Error('req.session can only be set as null or an object.');
103 });
104
105 onHeaders(res, function setHeaders() {
106 if (sess === undefined) {
107 // not accessed
108 return;
109 }
110
111 try {
112 if (sess === false) {
113 // remove
114 cookies.set(name, '', req.sessionOptions)
115 } else if (!json && !sess.length) {
116 // do nothing if new and not populated
117 } else if (sess.changed(json)) {
118 // save
119 sess.save();
120 }
121 } catch (e) {
122 debug('error saving session %s', e.message);
123 }
124 });
125
126 next();
127 }
128};
129
130/**
131 * Session model.
132 *
133 * @param {Context} ctx
134 * @param {Object} obj
135 * @private
136 */
137
138function Session(ctx, obj) {
139 this._ctx = ctx
140
141 Object.defineProperty(this, 'isNew', {
142 value: !obj
143 })
144
145 if (obj) {
146 for (var key in obj) {
147 this[key] = obj[key]
148 }
149 }
150}
151
152/**
153 * JSON representation of the session.
154 *
155 * @return {Object}
156 * @public
157 */
158
159Session.prototype.inspect =
160Session.prototype.toJSON = function toJSON() {
161 var keys = Object.keys(this)
162 var obj = {}
163
164 for (var i = 0; i < keys.length; i++) {
165 var key = keys[i]
166
167 if (key[0] !== '_') {
168 obj[key] = this[key]
169 }
170 }
171
172 return obj
173}
174
175/**
176 * Check if the session has changed relative to the `prev`
177 * JSON value from the request.
178 *
179 * @param {String} [prev]
180 * @return {Boolean}
181 * @private
182 */
183
184Session.prototype.changed = function(prev){
185 if (!prev) return true;
186 this._json = encode(this);
187 return this._json != prev;
188};
189
190/**
191 * Return how many values there are in the session object.
192 * Used to see if it's "populated".
193 *
194 * @return {Number}
195 * @public
196 */
197
198Session.prototype.__defineGetter__('length', function(){
199 return Object.keys(this.toJSON()).length;
200});
201
202/**
203 * populated flag, which is just a boolean alias of .length.
204 *
205 * @return {Boolean}
206 * @public
207 */
208
209Session.prototype.__defineGetter__('populated', function(){
210 return !!this.length;
211});
212
213/**
214 * Save session changes by performing a Set-Cookie.
215 *
216 * @private
217 */
218
219Session.prototype.save = function(){
220 var ctx = this._ctx;
221 var json = this._json || encode(this);
222 var opts = ctx.sessionOptions;
223 var name = ctx.sessionKey;
224
225 debug('save %s', json);
226 ctx.sessionCookies.set(name, json, opts);
227};
228
229/**
230 * Decode the base64 cookie value to an object.
231 *
232 * @param {String} string
233 * @return {Object}
234 * @private
235 */
236
237function decode(string) {
238 var body = new Buffer(string, 'base64').toString('utf8');
239 return JSON.parse(body);
240}
241
242/**
243 * Encode an object into a base64-encoded JSON string.
244 *
245 * @param {Object} body
246 * @return {String}
247 * @private
248 */
249
250function encode(body) {
251 var str = JSON.stringify(body)
252 return new Buffer(str).toString('base64')
253}