123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674 |
- /*!
- * express-session
- * Copyright(c) 2010 Sencha Inc.
- * Copyright(c) 2011 TJ Holowaychuk
- * Copyright(c) 2014-2015 Douglas Christopher Wilson
- * MIT Licensed
- */
- 'use strict';
- /**
- * 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 (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)
- }
- // 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, function () {
- wrapmethods(req.session)
- callback.apply(this, arguments)
- })
- }
- 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 && 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;
- }
|