diff --git a/README.md b/README.md index 239d49b7..e7b83428 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,24 @@ app.use(session({ })) ``` +##### sessionid + +Function to manually set the sessionID. This is most helpful for environments where +cookies are not possible. When the expression returns no value, the regular sessionID +will be used or generated. + +**NOTE** be careful with unsecured connections. When you don't use SSL, a man in the +middle could intercept a sessionID and pick up a session from there. + +```js +app.use(session({ + sessionid: function(req) { + return req.query.sessionID; + }, + secret: 'keyboard cat' +})) +``` + ##### name The name of the session ID cookie to set in the response (and read from in the diff --git a/index.js b/index.js index 6f6d1b52..732bdbad 100644 --- a/index.js +++ b/index.js @@ -17,13 +17,13 @@ var debug = require('debug')('express-session'); var deprecate = require('depd')('express-session'); var parseUrl = require('parseurl'); var uid = require('uid-safe').sync - , onHeaders = require('on-headers') - , signature = require('cookie-signature') + , onHeaders = require('on-headers') + , signature = require('cookie-signature') var Session = require('./session/session') - , MemoryStore = require('./session/memory') - , Cookie = require('./session/cookie') - , Store = require('./session/store') + , MemoryStore = require('./session/memory') + , Cookie = require('./session/cookie') + , Store = require('./session/store') // environment @@ -50,8 +50,8 @@ exports.MemoryStore = MemoryStore; */ var warning = 'Warning: connect.session() MemoryStore is not\n' - + 'designed for a production environment, as it will leak\n' - + 'memory, and will not scale past a single process.'; + + 'designed for a production environment, as it will leak\n' + + 'memory, and will not scale past a single process.'; /** * Node.js 0.8+ async implementation. @@ -60,8 +60,8 @@ var warning = 'Warning: connect.session() MemoryStore is not\n' /* istanbul ignore next */ var defer = typeof setImmediate === 'function' - ? setImmediate - : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } + ? setImmediate + : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } /** * Setup session store with the given `options`. @@ -74,6 +74,7 @@ var defer = typeof setImmediate === 'function' * @param {Boolean} [options.resave] Resave unmodified sessions back to the store * @param {Boolean} [options.rolling] Enable/disable rolling session expiration * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store + * @param {Function} [options.sessionid] Manually set session ID * @param {String|Array} [options.secret] Secret for signing session ID * @param {Object} [options.store=MemoryStore] Session store * @param {String} [options.unset] @@ -82,357 +83,360 @@ var defer = typeof setImmediate === 'function' */ function session(options){ - var options = options || {} - // name - previously "options.key" - , name = options.name || options.key || 'connect.sid' - , store = options.store || new MemoryStore - , cookie = options.cookie || {} - , trustProxy = options.proxy - , storeReady = true - , rollingSessions = options.rolling || false; - var resaveSession = options.resave; - var saveUninitializedSession = options.saveUninitialized; - var secret = options.secret; - - var generateId = options.genid || generateSessionId; - - if (typeof generateId !== 'function') { - throw new TypeError('genid option must be a function'); - } - - if (resaveSession === undefined) { - deprecate('undefined resave option; provide resave option'); - resaveSession = true; - } - - if (saveUninitializedSession === undefined) { - deprecate('undefined saveUninitialized option; provide saveUninitialized option'); - saveUninitializedSession = true; - } - - if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') { - throw new TypeError('unset option must be "destroy" or "keep"'); - } - - // TODO: switch to "destroy" on next major - var unsetDestroy = options.unset === 'destroy'; - - if (Array.isArray(secret) && secret.length === 0) { - throw new TypeError('secret option array must contain one or more strings'); - } - - if (secret && !Array.isArray(secret)) { - secret = [secret]; - } - - if (!secret) { - deprecate('req.secret; provide secret option'); - } - - // notify user that this store is not - // meant for a production environment - if ('production' == env && store instanceof MemoryStore) { - console.warn(warning); - } - - // generates the new session - store.generate = function(req){ - req.sessionID = generateId(req); - req.session = new Session(req); - req.session.cookie = new Cookie(cookie); - }; - - var storeImplementsTouch = typeof store.touch === 'function'; - store.on('disconnect', function(){ storeReady = false; }); - store.on('connect', function(){ storeReady = true; }); - - return function session(req, res, next) { - // self-awareness - if (req.session) return next(); - - // Handle connection as if there is no session if - // the store has temporarily disconnected etc - if (!storeReady) return debug('store is disconnected'), next(); - - // pathname mismatch - var originalPath = parseUrl.original(req).pathname; - if (0 != originalPath.indexOf(cookie.path || '/')) return next(); - - // ensure a secret is available or bail - if (!secret && !req.secret) { - next(new Error('secret option required for sessions')); - return; + var options = options || {} + // name - previously "options.key" + , name = options.name || options.key || 'connect.sid' + , store = options.store || new MemoryStore + , cookie = options.cookie || {} + , trustProxy = options.proxy + , storeReady = true + , rollingSessions = options.rolling || false; + var resaveSession = options.resave; + var tokenHeaderId = options.tokenheaderid + var saveUninitializedSession = options.saveUninitialized; + var secret = options.secret; + + var generateId = options.genid || generateSessionId; + + var getManualSessionId = options.sessionid || function(req) {}; + + if (typeof generateId !== 'function') { + throw new TypeError('genid option must be a function'); } - // backwards compatibility for signed cookies - // req.secret is passed from the cookie parser middleware - var secrets = secret || [req.secret]; - - var originalHash; - var originalId; - var savedHash; - - // expose store - req.sessionStore = store; - - // get the session ID from the cookie - var cookieId = req.sessionID = getcookie(req, name, secrets); + if (resaveSession === undefined) { + deprecate('undefined resave option; provide resave option'); + resaveSession = true; + } - // set-cookie - onHeaders(res, function(){ - if (!req.session) { - debug('no session'); - return; - } + if (saveUninitializedSession === undefined) { + deprecate('undefined saveUninitialized option; provide saveUninitialized option'); + saveUninitializedSession = true; + } - var cookie = req.session.cookie; + if (options.unset && options.unset !== 'destroy' && options.unset !== 'keep') { + throw new TypeError('unset option must be "destroy" or "keep"'); + } - // only send secure cookies via https - if (cookie.secure && !issecure(req, trustProxy)) { - debug('not secured'); - return; - } + // TODO: switch to "destroy" on next major + var unsetDestroy = options.unset === 'destroy'; - if (!shouldSetCookie(req)) { - return; - } + if (Array.isArray(secret) && secret.length === 0) { + throw new TypeError('secret option array must contain one or more strings'); + } - setcookie(res, name, req.sessionID, secrets[0], cookie.data); - }); + if (secret && !Array.isArray(secret)) { + secret = [secret]; + } - // proxy end() to commit the session - var _end = res.end; - var _write = res.write; - var ended = false; - res.end = function end(chunk, encoding) { - if (ended) { - return false; - } + if (!secret) { + deprecate('req.secret; provide secret option'); + } - ended = true; + // notify user that this store is not + // meant for a production environment + if ('production' == env && store instanceof MemoryStore) { + console.warn(warning); + } - var ret; - var sync = true; + // generates the new session + store.generate = function(req){ + req.sessionID = getManualSessionId(req) ? getManualSessionId(req) : generateId(req); + req.session = new Session(req); + req.session.cookie = new Cookie(cookie); + }; - function writeend() { - if (sync) { - ret = _end.call(res, chunk, encoding); - sync = false; - return; - } + var storeImplementsTouch = typeof store.touch === 'function'; + store.on('disconnect', function(){ storeReady = false; }); + store.on('connect', function(){ storeReady = true; }); - _end.call(res); - } + return function session(req, res, next) { + // self-awareness + if (req.session) return next(); - function writetop() { - if (!sync) { - return ret; - } + // Handle connection as if there is no session if + // the store has temporarily disconnected etc + if (!storeReady) return debug('store is disconnected'), next(); - if (chunk == null) { - ret = true; - return ret; - } + // pathname mismatch + var originalPath = parseUrl.original(req).pathname; + if (0 != originalPath.indexOf(cookie.path || '/')) return next(); - var contentLength = Number(res.getHeader('Content-Length')); - - if (!isNaN(contentLength) && contentLength > 0) { - // measure chunk - chunk = !Buffer.isBuffer(chunk) - ? new Buffer(chunk, encoding) - : chunk; - encoding = undefined; - - if (chunk.length !== 0) { - debug('split response'); - ret = _write.call(res, chunk.slice(0, chunk.length - 1)); - chunk = chunk.slice(chunk.length - 1, chunk.length); - return ret; - } + // ensure a secret is available or bail + if (!secret && !req.secret) { + next(new Error('secret option required for sessions')); + return; } - ret = _write.call(res, chunk, encoding); - sync = false; + // backwards compatibility for signed cookies + // req.secret is passed from the cookie parser middleware + var secrets = secret || [req.secret]; - return ret; - } + var originalHash; + var originalId; + var savedHash; - if (shouldDestroy(req)) { - // destroy session - debug('destroying'); - store.destroy(req.sessionID, function ondestroy(err) { - if (err) { - defer(next, err); - } - - debug('destroyed'); - writeend(); - }); + // expose store + req.sessionStore = store; - return writetop(); - } + // get the session ID from the cookie / header + var sessionId = req.sessionID = getManualSessionId(req) ? getManualSessionId(req) : getSessionIdFromCookieOrHeader(req, name, secrets, tokenHeaderId); - // no session to save - if (!req.session) { - debug('no session'); - return _end.call(res, chunk, encoding); - } + // set-cookie + onHeaders(res, function(){ + if (!req.session) { + debug('no session'); + return; + } - // touch session - req.session.touch(); + var cookie = req.session.cookie; - if (shouldSave(req)) { - req.session.save(function onsave(err) { - if (err) { - defer(next, err); - } + // only send secure cookies via https + if (cookie.secure && !issecure(req, trustProxy)) { + debug('not secured'); + return; + } - writeend(); - }); + if (!shouldSetCookie(req)) { + return; + } - return writetop(); - } else if (storeImplementsTouch && shouldTouch(req)) { - // store implements touch method - debug('touching'); - store.touch(req.sessionID, req.session, function ontouch(err) { - if (err) { - defer(next, err); - } - - debug('touched'); - writeend(); + setSessionIdOnCookieOrHeader(res, name, req.sessionID, secrets[0], cookie.data, tokenHeaderId); }); - return writetop(); - } - - return _end.call(res, chunk, encoding); - }; - - // generate the session - function generate() { - store.generate(req); - originalId = req.sessionID; - originalHash = hash(req.session); - wrapmethods(req.session); - } - - // wrap session methods - function wrapmethods(sess) { - var _save = sess.save; - - function save() { - debug('saving %s', this.id); - savedHash = hash(this); - _save.apply(this, arguments); - } - - Object.defineProperty(sess, 'save', { - configurable: true, - enumerable: false, - value: save, - writable: true - }); - } - - // check if session has been modified - function isModified(sess) { - return originalId !== sess.id || originalHash !== hash(sess); - } - - // check if session has been saved - function isSaved(sess) { - return originalId === sess.id && savedHash === hash(sess); - } - - // determine if session should be destroyed - function shouldDestroy(req) { - return req.sessionID && unsetDestroy && req.session == null; - } - - // determine if session should be saved to store - function shouldSave(req) { - // cannot set cookie without a session ID - if (typeof req.sessionID !== 'string') { - debug('session ignored because of bogus req.sessionID %o', req.sessionID); - return false; - } - - return !saveUninitializedSession && cookieId !== req.sessionID - ? isModified(req.session) - : !isSaved(req.session) - } - - // determine if session should be touched - function shouldTouch(req) { - // cannot set cookie without a session ID - if (typeof req.sessionID !== 'string') { - debug('session ignored because of bogus req.sessionID %o', req.sessionID); - return false; - } + // proxy end() to commit the session + var _end = res.end; + var _write = res.write; + var ended = false; + res.end = function end(chunk, encoding) { + if (ended) { + return false; + } + + ended = true; + + var ret; + var sync = true; + + function writeend() { + if (sync) { + ret = _end.call(res, chunk, encoding); + sync = false; + return; + } + + _end.call(res); + } + + function writetop() { + if (!sync) { + return ret; + } + + if (chunk == null) { + ret = true; + return ret; + } + + var contentLength = Number(res.getHeader('Content-Length')); + + if (!isNaN(contentLength) && contentLength > 0) { + // measure chunk + chunk = !Buffer.isBuffer(chunk) + ? new Buffer(chunk, encoding) + : chunk; + encoding = undefined; + + if (chunk.length !== 0) { + debug('split response'); + ret = _write.call(res, chunk.slice(0, chunk.length - 1)); + chunk = chunk.slice(chunk.length - 1, chunk.length); + return ret; + } + } + + ret = _write.call(res, chunk, encoding); + sync = false; + + return ret; + } + + if (shouldDestroy(req)) { + // destroy session + debug('destroying'); + store.destroy(req.sessionID, function ondestroy(err) { + if (err) { + defer(next, err); + } + + debug('destroyed'); + writeend(); + }); + + return writetop(); + } + + // no session to save + if (!req.session) { + debug('no session'); + return _end.call(res, chunk, encoding); + } + + // touch session + req.session.touch(); + + if (shouldSave(req)) { + req.session.save(function onsave(err) { + if (err) { + defer(next, err); + } + + writeend(); + }); + + return writetop(); + } else if (storeImplementsTouch && shouldTouch(req)) { + // store implements touch method + debug('touching'); + store.touch(req.sessionID, req.session, function ontouch(err) { + if (err) { + defer(next, err); + } + + debug('touched'); + writeend(); + }); + + return writetop(); + } + + return _end.call(res, chunk, encoding); + }; + + // generate the session + function generate() { + store.generate(req); + originalId = req.sessionID; + originalHash = hash(req.session); + wrapmethods(req.session); + } - return cookieId === req.sessionID && !shouldSave(req); - } + // wrap session methods + function wrapmethods(sess) { + var _save = sess.save; + + function save() { + debug('saving %s', this.id); + savedHash = hash(this); + _save.apply(this, arguments); + } + + Object.defineProperty(sess, 'save', { + configurable: true, + enumerable: false, + value: save, + writable: true + }); + } - // determine if cookie should be set on response - function shouldSetCookie(req) { - // cannot set cookie without a session ID - if (typeof req.sessionID !== 'string') { - return false; - } + // check if session has been modified + function isModified(sess) { + return originalId !== sess.id || originalHash !== hash(sess); + } - // in case of rolling session, always reset the cookie - if (rollingSessions) { - return true; - } + // check if session has been saved + function isSaved(sess) { + return originalId === sess.id && savedHash === hash(sess); + } - return cookieId != req.sessionID - ? saveUninitializedSession || isModified(req.session) - : req.session.cookie.expires != null && isModified(req.session); - } + // determine if session should be destroyed + function shouldDestroy(req) { + return req.sessionID && unsetDestroy && req.session == null; + } - // generate a session if the browser doesn't send a sessionID - if (!req.sessionID) { - debug('no SID sent, generating session'); - generate(); - next(); - return; - } + // determine if session should be saved to store + function shouldSave(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + debug('session ignored because of bogus req.sessionID %o', req.sessionID); + return false; + } + + return !saveUninitializedSession && sessionId !== req.sessionID + ? isModified(req.session) + : !isSaved(req.session) + } - // generate the session object - debug('fetching %s', req.sessionID); - store.get(req.sessionID, function(err, sess){ - // error handling - if (err) { - debug('error %j', err); + // determine if session should be touched + function shouldTouch(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + debug('session ignored because of bogus req.sessionID %o', req.sessionID); + return false; + } - if (err.code !== 'ENOENT') { - next(err); - return; + return sessionId === req.sessionID && !shouldSave(req); } - generate(); - // no session - } else if (!sess) { - debug('no session found'); - generate(); - // populate req.session - } else { - debug('session found'); - store.createSession(req, sess); - originalId = req.sessionID; - originalHash = hash(sess); - - if (!resaveSession) { - savedHash = originalHash + // determine if cookie should be set on response + function shouldSetCookie(req) { + // cannot set cookie without a session ID + if (typeof req.sessionID !== 'string') { + return false; + } + + // in case of rolling session, always reset the cookie + if (rollingSessions) { + return true; + } + + return sessionId != req.sessionID + ? saveUninitializedSession || isModified(req.session) + : req.session.cookie.expires != null && isModified(req.session); } - wrapmethods(req.session); - } + // generate a session if the browser doesn't send a sessionID + if (!req.sessionID) { + debug('no SID sent, generating session'); + generate(); + next(); + return; + } - next(); - }); - }; + // generate the session object + debug('fetching %s', req.sessionID); + store.get(req.sessionID, function(err, sess){ + // error handling + if (err) { + debug('error %j', err); + + if (err.code !== 'ENOENT') { + next(err); + return; + } + + generate(); + // no session + } else if (!sess) { + debug('no session found'); + generate(); + // populate req.session + } else { + debug('session found'); + store.createSession(req, sess); + originalId = req.sessionID; + originalHash = hash(sess); + + if (!resaveSession) { + savedHash = originalHash + } + + wrapmethods(req.session); + } + + next(); + }); + }; }; /** @@ -443,7 +447,7 @@ function session(options){ */ function generateSessionId(sess) { - return uid(24); + return uid(128); } /** @@ -453,63 +457,63 @@ function generateSessionId(sess) { * @private */ -function getcookie(req, name, secrets) { - var header = req.headers.cookie; - var raw; - var val; +function getSessionIdFromCookieOrHeader(req, name, secrets, tokenHeaderId) { + var header = req.headers.cookie || req.get(tokenHeaderId); + var raw; + var val; - // read from cookie header - if (header) { - var cookies = cookie.parse(header); + // read from cookie header + if (header) { + var cookies = cookie.parse(header); - raw = cookies[name]; + raw = cookies[name]; - if (raw) { - if (raw.substr(0, 2) === 's:') { - val = unsigncookie(raw.slice(2), secrets); + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); - if (val === false) { - debug('cookie signature invalid'); - val = undefined; + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned') + } } - } else { - debug('cookie unsigned') - } - } - } - - // back-compat read from cookieParser() signedCookies data - if (!val && req.signedCookies) { - val = req.signedCookies[name]; - - if (val) { - deprecate('cookie should be available in req.headers.cookie'); } - } - - // back-compat read from cookieParser() cookies data - if (!val && req.cookies) { - raw = req.cookies[name]; - if (raw) { - if (raw.substr(0, 2) === 's:') { - val = unsigncookie(raw.slice(2), secrets); + // back-compat read from cookieParser() signedCookies data + if (!val && req.signedCookies) { + val = req.signedCookies[name]; if (val) { - deprecate('cookie should be available in req.headers.cookie'); + deprecate('cookie should be available in req.headers.cookie'); } + } - if (val === false) { - debug('cookie signature invalid'); - val = undefined; + // back-compat read from cookieParser() cookies data + if (!val && req.cookies) { + raw = req.cookies[name]; + + if (raw) { + if (raw.substr(0, 2) === 's:') { + val = unsigncookie(raw.slice(2), secrets); + + if (val) { + deprecate('cookie should be available in req.headers.cookie'); + } + + if (val === false) { + debug('cookie signature invalid'); + val = undefined; + } + } else { + debug('cookie unsigned') + } } - } else { - debug('cookie unsigned') - } } - } - return val; + return val; } /** @@ -521,11 +525,11 @@ function getcookie(req, name, secrets) { */ function hash(sess) { - return crc(JSON.stringify(sess, function (key, val) { - if (key !== 'cookie') { - return val; - } - })); + return crc(JSON.stringify(sess, function (key, val) { + if (key !== 'cookie') { + return val; + } + })); } /** @@ -538,32 +542,32 @@ function hash(sess) { */ function issecure(req, trustProxy) { - // socket is https server - if (req.connection && req.connection.encrypted) { - return true; - } + // socket is https server + if (req.connection && req.connection.encrypted) { + return true; + } - // do not trust proxy - if (trustProxy === false) { - return false; - } - - // no explicit trust; try req.secure from express - if (trustProxy !== true) { - var secure = req.secure; - return typeof secure === 'boolean' - ? secure - : false; - } - - // read the proto from x-forwarded-proto header - var header = req.headers['x-forwarded-proto'] || ''; - var index = header.indexOf(','); - var proto = index !== -1 - ? header.substr(0, index).toLowerCase().trim() - : header.toLowerCase().trim() - - return proto === 'https'; + // do not trust proxy + if (trustProxy === false) { + return false; + } + + // no explicit trust; try req.secure from express + if (trustProxy !== true) { + var secure = req.secure; + return typeof secure === 'boolean' + ? secure + : false; + } + + // read the proto from x-forwarded-proto header + var header = req.headers['x-forwarded-proto'] || ''; + var index = header.indexOf(','); + var proto = index !== -1 + ? header.substr(0, index).toLowerCase().trim() + : header.toLowerCase().trim() + + return proto === 'https'; } /** @@ -572,18 +576,19 @@ function issecure(req, trustProxy) { * @private */ -function setcookie(res, name, val, secret, options) { - var signed = 's:' + signature.sign(val, secret); - var data = cookie.serialize(name, signed, options); +function setSessionIdOnCookieOrHeader(res, name, val, secret, options, tokenHeaderId) { + var signed = 's:' + signature.sign(val, secret); + var data = cookie.serialize(name, signed, options); - debug('set-cookie %s', data); + debug('set-cookie %s', data); - var prev = res.getHeader('set-cookie') || []; - var header = Array.isArray(prev) ? prev.concat(data) - : Array.isArray(data) ? [prev].concat(data) - : [prev, data]; + var prev = res.getHeader('set-cookie') || []; + var header = Array.isArray(prev) ? prev.concat(data) + : Array.isArray(data) ? [prev].concat(data) + : [prev, data]; - res.setHeader('set-cookie', header) + res.setHeader('set-cookie', header); + res.setHeader(tokenHeaderId, val); } /** @@ -595,13 +600,13 @@ function setcookie(res, name, val, secret, options) { * @private */ function unsigncookie(val, secrets) { - for (var i = 0; i < secrets.length; i++) { - var result = signature.unsign(val, secrets[i]); + for (var i = 0; i < secrets.length; i++) { + var result = signature.unsign(val, secrets[i]); - if (result !== false) { - return result; + if (result !== false) { + return result; + } } - } - return false; + return false; }