3 * Copyright(c) 2010 Sencha Inc.
4 * Copyright(c) 2011 TJ Holowaychuk
5 * Copyright(c) 2014-2015 Douglas Christopher Wilson
10 * Module dependencies.
14 var cookie = require('cookie');
15 var crc = require('crc').crc32;
16 var debug = require('debug')('express-session');
17 var deprecate = require('depd')('express-session');
18 var parseUrl = require('parseurl');
19 var uid = require('uid-safe').sync
20 , onHeaders = require('on-headers')
21 , signature = require('cookie-signature')
23 var Session = require('./session/session')
24 , MemoryStore = require('./session/memory')
25 , Cookie = require('./session/cookie')
26 , Store = require('./session/store')
30 var env = process.env.NODE_ENV;
33 * Expose the middleware.
36 exports = module.exports = session;
39 * Expose constructors.
42 exports.Store = Store;
43 exports.Cookie = Cookie;
44 exports.Session = Session;
45 exports.MemoryStore = MemoryStore;
48 * Warning message for `MemoryStore` usage in production.
52 var warning = 'Warning: connect.session() MemoryStore is not\n'
53 + 'designed for a production environment, as it will leak\n'
54 + 'memory, and will not scale past a single process.';
57 * Node.js 0.8+ async implementation.
61 /* istanbul ignore next */
62 var defer = typeof setImmediate === 'function'
64 : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
67 * Setup session store with the given `options`.
69 * @param {Object} [options]
70 * @param {Object} [options.cookie] Options for cookie
71 * @param {Function} [options.genid]
72 * @param {String} [options.name=connect.sid] Session ID cookie name
73 * @param {Boolean} [options.proxy]
74 * @param {Boolean} [options.resave] Resave unmodified sessions back to the store
75 * @param {Boolean} [options.rolling] Enable/disable rolling session expiration
76 * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store
77 * @param {String|Array} [options.secret] Secret for signing session ID
78 * @param {Object} [options.store=MemoryStore] Session store
79 * @param {String} [options.unset]
80 * @return {Function} middleware
84 function session(options){
85 var options = options || {}
86 // name - previously "options.key"
87 , name = options.name || options.key || 'connect.sid'
88 , store = options.store || new MemoryStore
89 , cookie = options.cookie || {}
90 , trustProxy = options.proxy
92 , rollingSessions = options.rolling || false;
93 var resaveSession = options.resave;
94 var saveUninitializedSession = options.saveUninitialized;
95 var secret = options.secret;
97 var generateId = options.genid || generateSessionId;
99 if (typeof generateId !== 'function') {
100 throw new TypeError('genid option must be a function');
103 if (resaveSession === undefined) {
104 deprecate('undefined resave option; provide resave option');
105 resaveSession = true;
108 if (saveUninitializedSession === undefined) {
109 deprecate('undefined saveUninitialized option; provide saveUninitialized option');
110 saveUninitializedSession = true;
113 if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') {
114 throw new TypeError('unset option must be "destroy" or "keep"');
117 // TODO: switch to "destroy" on next major
118 var unsetDestroy = options.unset === 'destroy';
120 if (Array.isArray(secret) && secret.length === 0) {
121 throw new TypeError('secret option array must contain one or more strings');
124 if (secret && !Array.isArray(secret)) {
129 deprecate('req.secret; provide secret option');
132 // notify user that this store is not
133 // meant for a production environment
134 if ('production' == env && store instanceof MemoryStore) {
135 console.warn(warning);
138 // generates the new session
139 store.generate = function(req){
140 req.sessionID = generateId(req);
141 req.session = new Session(req);
142 req.session.cookie = new Cookie(cookie);
145 var storeImplementsTouch = typeof store.touch === 'function';
146 store.on('disconnect', function(){ storeReady = false; });
147 store.on('connect', function(){ storeReady = true; });
149 return function session(req, res, next) {
151 if (req.session) return next();
153 // Handle connection as if there is no session if
154 // the store has temporarily disconnected etc
155 if (!storeReady) return debug('store is disconnected'), next();
158 var originalPath = parseUrl.original(req).pathname;
159 if (0 != originalPath.indexOf(cookie.path || '/')) return next();
161 // ensure a secret is available or bail
162 if (!secret && !req.secret) {
163 next(new Error('secret option required for sessions'));
167 // backwards compatibility for signed cookies
168 // req.secret is passed from the cookie parser middleware
169 var secrets = secret || [req.secret];
176 req.sessionStore = store;
178 // get the session ID from the cookie
179 var cookieId = req.sessionID = getcookie(req, name, secrets);
182 onHeaders(res, function(){
188 var cookie = req.session.cookie;
190 // only send secure cookies via https
191 if (cookie.secure && !issecure(req, trustProxy)) {
192 debug('not secured');
196 if (!shouldSetCookie(req)) {
200 setcookie(res, name, req.sessionID, secrets[0], cookie.data);
203 // proxy end() to commit the session
205 var _write = res.write;
207 res.end = function end(chunk, encoding) {
217 function writeend() {
219 ret = _end.call(res, chunk, encoding);
227 function writetop() {
237 var contentLength = Number(res.getHeader('Content-Length'));
239 if (!isNaN(contentLength) && contentLength > 0) {
241 chunk = !Buffer.isBuffer(chunk)
242 ? new Buffer(chunk, encoding)
244 encoding = undefined;
246 if (chunk.length !== 0) {
247 debug('split response');
248 ret = _write.call(res, chunk.slice(0, chunk.length - 1));
249 chunk = chunk.slice(chunk.length - 1, chunk.length);
254 ret = _write.call(res, chunk, encoding);
260 if (shouldDestroy(req)) {
263 store.destroy(req.sessionID, function ondestroy(err) {
275 // no session to save
278 return _end.call(res, chunk, encoding);
284 if (shouldSave(req)) {
285 req.session.save(function onsave(err) {
294 } else if (storeImplementsTouch && shouldTouch(req)) {
295 // store implements touch method
297 store.touch(req.sessionID, req.session, function ontouch(err) {
309 return _end.call(res, chunk, encoding);
312 // generate the session
313 function generate() {
315 originalId = req.sessionID;
316 originalHash = hash(req.session);
317 wrapmethods(req.session);
320 // wrap session methods
321 function wrapmethods(sess) {
322 var _save = sess.save;
325 debug('saving %s', this.id);
326 savedHash = hash(this);
327 _save.apply(this, arguments);
330 Object.defineProperty(sess, 'save', {
338 // check if session has been modified
339 function isModified(sess) {
340 return originalId !== sess.id || originalHash !== hash(sess);
343 // check if session has been saved
344 function isSaved(sess) {
345 return originalId === sess.id && savedHash === hash(sess);
348 // determine if session should be destroyed
349 function shouldDestroy(req) {
350 return req.sessionID && unsetDestroy && req.session == null;
353 // determine if session should be saved to store
354 function shouldSave(req) {
355 // cannot set cookie without a session ID
356 if (typeof req.sessionID !== 'string') {
357 debug('session ignored because of bogus req.sessionID %o', req.sessionID);
361 return !saveUninitializedSession && cookieId !== req.sessionID
362 ? isModified(req.session)
363 : !isSaved(req.session)
366 // determine if session should be touched
367 function shouldTouch(req) {
368 // cannot set cookie without a session ID
369 if (typeof req.sessionID !== 'string') {
370 debug('session ignored because of bogus req.sessionID %o', req.sessionID);
374 return cookieId === req.sessionID && !shouldSave(req);
377 // determine if cookie should be set on response
378 function shouldSetCookie(req) {
379 // cannot set cookie without a session ID
380 if (typeof req.sessionID !== 'string') {
384 // in case of rolling session, always reset the cookie
385 if (rollingSessions) {
389 return cookieId != req.sessionID
390 ? saveUninitializedSession || isModified(req.session)
391 : req.session.cookie.expires != null && isModified(req.session);
394 // generate a session if the browser doesn't send a sessionID
395 if (!req.sessionID) {
396 debug('no SID sent, generating session');
402 // generate the session object
403 debug('fetching %s', req.sessionID);
404 store.get(req.sessionID, function(err, sess){
407 debug('error %j', err);
409 if (err.code !== 'ENOENT') {
417 debug('no session found');
419 // populate req.session
421 debug('session found');
422 store.createSession(req, sess);
423 originalId = req.sessionID;
424 originalHash = hash(sess);
426 if (!resaveSession) {
427 savedHash = originalHash
430 wrapmethods(req.session);
439 * Generate a session ID for a new session.
445 function generateSessionId(sess) {
450 * Get the session ID cookie from request.
456 function getcookie(req, name, secrets) {
457 var header = req.headers.cookie;
461 // read from cookie header
463 var cookies = cookie.parse(header);
468 if (raw.substr(0, 2) === 's:') {
469 val = unsigncookie(raw.slice(2), secrets);
472 debug('cookie signature invalid');
476 debug('cookie unsigned')
481 // back-compat read from cookieParser() signedCookies data
482 if (!val && req.signedCookies) {
483 val = req.signedCookies[name];
486 deprecate('cookie should be available in req.headers.cookie');
490 // back-compat read from cookieParser() cookies data
491 if (!val && req.cookies) {
492 raw = req.cookies[name];
495 if (raw.substr(0, 2) === 's:') {
496 val = unsigncookie(raw.slice(2), secrets);
499 deprecate('cookie should be available in req.headers.cookie');
503 debug('cookie signature invalid');
507 debug('cookie unsigned')
516 * Hash the given `sess` object omitting changes to `.cookie`.
518 * @param {Object} sess
523 function hash(sess) {
524 return crc(JSON.stringify(sess, function (key, val) {
525 if (key !== 'cookie') {
532 * Determine if request is secure.
534 * @param {Object} req
535 * @param {Boolean} [trustProxy]
540 function issecure(req, trustProxy) {
541 // socket is https server
542 if (req.connection && req.connection.encrypted) {
546 // do not trust proxy
547 if (trustProxy === false) {
551 // no explicit trust; try req.secure from express
552 if (trustProxy !== true) {
553 var secure = req.secure;
554 return typeof secure === 'boolean'
559 // read the proto from x-forwarded-proto header
560 var header = req.headers['x-forwarded-proto'] || '';
561 var index = header.indexOf(',');
562 var proto = index !== -1
563 ? header.substr(0, index).toLowerCase().trim()
564 : header.toLowerCase().trim()
566 return proto === 'https';
570 * Set cookie on response.
575 function setcookie(res, name, val, secret, options) {
576 var signed = 's:' + signature.sign(val, secret);
577 var data = cookie.serialize(name, signed, options);
579 debug('set-cookie %s', data);
581 var prev = res.getHeader('set-cookie') || [];
582 var header = Array.isArray(prev) ? prev.concat(data)
583 : Array.isArray(data) ? [prev].concat(data)
586 res.setHeader('set-cookie', header)
590 * Verify and decode the given `val` with `secrets`.
592 * @param {String} val
593 * @param {Array} secrets
594 * @returns {String|Boolean}
597 function unsigncookie(val, secrets) {
598 for (var i = 0; i < secrets.length; i++) {
599 var result = signature.unsign(val, secrets[i]);
601 if (result !== false) {