Spaces:
Running
Running
/*! | |
* express-session | |
* Copyright(c) 2010 Sencha Inc. | |
* Copyright(c) 2011 TJ Holowaychuk | |
* Copyright(c) 2014-2015 Douglas Christopher Wilson | |
* MIT Licensed | |
*/ | |
; | |
/** | |
* Module dependencies. | |
* @private | |
*/ | |
var Buffer = require('safe-buffer').Buffer | |
var cookie = require('cookie'); | |
var crypto = require('crypto') | |
var debug = require('debug')('express-session'); | |
var deprecate = require('depd')('express-session'); | |
var onHeaders = require('on-headers') | |
var parseUrl = require('parseurl'); | |
var signature = require('cookie-signature') | |
var uid = require('uid-safe').sync | |
var Cookie = require('./session/cookie') | |
var MemoryStore = require('./session/memory') | |
var Session = require('./session/session') | |
var Store = require('./session/store') | |
// environment | |
var env = process.env.NODE_ENV; | |
/** | |
* Expose the middleware. | |
*/ | |
exports = module.exports = session; | |
/** | |
* Expose constructors. | |
*/ | |
exports.Store = Store; | |
exports.Cookie = Cookie; | |
exports.Session = Session; | |
exports.MemoryStore = MemoryStore; | |
/** | |
* Warning message for `MemoryStore` usage in production. | |
* @private | |
*/ | |
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.'; | |
/** | |
* Node.js 0.8+ async implementation. | |
* @private | |
*/ | |
/* istanbul ignore next */ | |
var defer = typeof setImmediate === 'function' | |
? setImmediate | |
: function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } | |
/** | |
* Setup session store with the given `options`. | |
* | |
* @param {Object} [options] | |
* @param {Object} [options.cookie] Options for cookie | |
* @param {Function} [options.genid] | |
* @param {String} [options.name=connect.sid] Session ID cookie name | |
* @param {Boolean} [options.proxy] | |
* @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 {String|Array} [options.secret] Secret for signing session ID | |
* @param {Object} [options.store=MemoryStore] Session store | |
* @param {String} [options.unset] | |
* @return {Function} middleware | |
* @public | |
*/ | |
function session(options) { | |
var opts = options || {} | |
// get the cookie options | |
var cookieOptions = opts.cookie || {} | |
// get the session id generate function | |
var generateId = opts.genid || generateSessionId | |
// get the session cookie name | |
var name = opts.name || opts.key || 'connect.sid' | |
// get the session store | |
var store = opts.store || new MemoryStore() | |
// get the trust proxy setting | |
var trustProxy = opts.proxy | |
// get the resave session option | |
var resaveSession = opts.resave; | |
// get the rolling session option | |
var rollingSessions = Boolean(opts.rolling) | |
// get the save uninitialized session option | |
var saveUninitializedSession = opts.saveUninitialized | |
// get the cookie signing secret | |
var secret = opts.secret | |
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 (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') { | |
throw new TypeError('unset option must be "destroy" or "keep"'); | |
} | |
// TODO: switch to "destroy" on next major | |
var unsetDestroy = opts.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 | |
/* istanbul ignore next: not tested */ | |
if (env === 'production' && 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(cookieOptions); | |
if (cookieOptions.secure === 'auto') { | |
req.session.cookie.secure = issecure(req, trustProxy); | |
} | |
}; | |
var storeImplementsTouch = typeof store.touch === 'function'; | |
// register event listeners for the store to track readiness | |
var storeReady = true | |
store.on('disconnect', function ondisconnect() { | |
storeReady = false | |
}) | |
store.on('connect', function onconnect() { | |
storeReady = true | |
}) | |
return function session(req, res, next) { | |
// self-awareness | |
if (req.session) { | |
next() | |
return | |
} | |
// Handle connection as if there is no session if | |
// the store has temporarily disconnected etc | |
if (!storeReady) { | |
debug('store is disconnected') | |
next() | |
return | |
} | |
// pathname mismatch | |
var originalPath = parseUrl.original(req).pathname || '/' | |
if (originalPath.indexOf(cookieOptions.path || '/') !== 0) return next(); | |
// ensure a secret is available or bail | |
if (!secret && !req.secret) { | |
next(new Error('secret option required for sessions')); | |
return; | |
} | |
// 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; | |
var touched = false | |
// expose store | |
req.sessionStore = store; | |
// get the session ID from the cookie | |
var cookieId = req.sessionID = getcookie(req, name, secrets); | |
// set-cookie | |
onHeaders(res, function(){ | |
if (!req.session) { | |
debug('no session'); | |
return; | |
} | |
if (!shouldSetCookie(req)) { | |
return; | |
} | |
// only send secure cookies via https | |
if (req.session.cookie.secure && !issecure(req, trustProxy)) { | |
debug('not secured'); | |
return; | |
} | |
if (!touched) { | |
// touch session | |
req.session.touch() | |
touched = true | |
} | |
// set cookie | |
setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data); | |
}); | |
// 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 (!res._header) { | |
res._implicitHeader() | |
} | |
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) | |
? Buffer.from(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); | |
} | |
if (!touched) { | |
// touch session | |
req.session.touch() | |
touched = true | |
} | |
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); | |
} | |
// inflate the session | |
function inflate (req, sess) { | |
store.createSession(req, sess) | |
originalId = req.sessionID | |
originalHash = hash(sess) | |
if (!resaveSession) { | |
savedHash = originalHash | |
} | |
wrapmethods(req.session) | |
} | |
function rewrapmethods (sess, callback) { | |
return function () { | |
if (req.session !== sess) { | |
wrapmethods(req.session) | |
} | |
callback.apply(this, arguments) | |
} | |
} | |
// wrap session methods | |
function wrapmethods(sess) { | |
var _reload = sess.reload | |
var _save = sess.save; | |
function reload(callback) { | |
debug('reloading %s', this.id) | |
_reload.call(this, rewrapmethods(this, callback)) | |
} | |
function save() { | |
debug('saving %s', this.id); | |
savedHash = hash(this); | |
_save.apply(this, arguments); | |
} | |
Object.defineProperty(sess, 'reload', { | |
configurable: true, | |
enumerable: false, | |
value: reload, | |
writable: true | |
}) | |
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 && !savedHash && 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; | |
} | |
return cookieId === req.sessionID && !shouldSave(req); | |
} | |
// 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; | |
} | |
return cookieId !== req.sessionID | |
? saveUninitializedSession || isModified(req.session) | |
: rollingSessions || req.session.cookie.expires != null && isModified(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; | |
} | |
// generate the session object | |
debug('fetching %s', req.sessionID); | |
store.get(req.sessionID, function(err, sess){ | |
// error handling | |
if (err && err.code !== 'ENOENT') { | |
debug('error %j', err); | |
next(err) | |
return | |
} | |
try { | |
if (err || !sess) { | |
debug('no session found') | |
generate() | |
} else { | |
debug('session found') | |
inflate(req, sess) | |
} | |
} catch (e) { | |
next(e) | |
return | |
} | |
next() | |
}); | |
}; | |
}; | |
/** | |
* Generate a session ID for a new session. | |
* | |
* @return {String} | |
* @private | |
*/ | |
function generateSessionId(sess) { | |
return uid(24); | |
} | |
/** | |
* Get the session ID cookie from request. | |
* | |
* @return {string} | |
* @private | |
*/ | |
function getcookie(req, name, secrets) { | |
var header = req.headers.cookie; | |
var raw; | |
var val; | |
// read from cookie header | |
if (header) { | |
var cookies = cookie.parse(header); | |
raw = cookies[name]; | |
if (raw) { | |
if (raw.substr(0, 2) === 's:') { | |
val = unsigncookie(raw.slice(2), secrets); | |
if (val === false) { | |
debug('cookie signature invalid'); | |
val = undefined; | |
} | |
} 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); | |
if (val) { | |
deprecate('cookie should be available in req.headers.cookie'); | |
} | |
if (val === false) { | |
debug('cookie signature invalid'); | |
val = undefined; | |
} | |
} else { | |
debug('cookie unsigned') | |
} | |
} | |
} | |
return val; | |
} | |
/** | |
* Hash the given `sess` object omitting changes to `.cookie`. | |
* | |
* @param {Object} sess | |
* @return {String} | |
* @private | |
*/ | |
function hash(sess) { | |
// serialize | |
var str = JSON.stringify(sess, function (key, val) { | |
// ignore sess.cookie property | |
if (this === sess && key === 'cookie') { | |
return | |
} | |
return val | |
}) | |
// hash | |
return crypto | |
.createHash('sha1') | |
.update(str, 'utf8') | |
.digest('hex') | |
} | |
/** | |
* Determine if request is secure. | |
* | |
* @param {Object} req | |
* @param {Boolean} [trustProxy] | |
* @return {Boolean} | |
* @private | |
*/ | |
function issecure(req, trustProxy) { | |
// 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) { | |
return req.secure === true | |
} | |
// 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'; | |
} | |
/** | |
* Set cookie on response. | |
* | |
* @private | |
*/ | |
function setcookie(res, name, val, secret, options) { | |
var signed = 's:' + signature.sign(val, secret); | |
var data = cookie.serialize(name, signed, options); | |
debug('set-cookie %s', data); | |
var prev = res.getHeader('Set-Cookie') || [] | |
var header = Array.isArray(prev) ? prev.concat(data) : [prev, data]; | |
res.setHeader('Set-Cookie', header) | |
} | |
/** | |
* Verify and decode the given `val` with `secrets`. | |
* | |
* @param {String} val | |
* @param {Array} secrets | |
* @returns {String|Boolean} | |
* @private | |
*/ | |
function unsigncookie(val, secrets) { | |
for (var i = 0; i < secrets.length; i++) { | |
var result = signature.unsign(val, secrets[i]); | |
if (result !== false) { | |
return result; | |
} | |
} | |
return false; | |
} | |