index.js
  1  /*!
  2   * express-session
  3   * Copyright(c) 2010 Sencha Inc.
  4   * Copyright(c) 2011 TJ Holowaychuk
  5   * Copyright(c) 2014-2015 Douglas Christopher Wilson
  6   * MIT Licensed
  7   */
  8  
  9  'use strict';
 10  
 11  /**
 12   * Module dependencies.
 13   * @private
 14   */
 15  
 16  var Buffer = require('safe-buffer').Buffer
 17  var cookie = require('cookie');
 18  var crypto = require('crypto')
 19  var debug = require('debug')('express-session');
 20  var deprecate = require('depd')('express-session');
 21  var onHeaders = require('on-headers')
 22  var parseUrl = require('parseurl');
 23  var signature = require('cookie-signature')
 24  var uid = require('uid-safe').sync
 25  
 26  var Cookie = require('./session/cookie')
 27  var MemoryStore = require('./session/memory')
 28  var Session = require('./session/session')
 29  var Store = require('./session/store')
 30  
 31  // environment
 32  
 33  var env = process.env.NODE_ENV;
 34  
 35  /**
 36   * Expose the middleware.
 37   */
 38  
 39  exports = module.exports = session;
 40  
 41  /**
 42   * Expose constructors.
 43   */
 44  
 45  exports.Store = Store;
 46  exports.Cookie = Cookie;
 47  exports.Session = Session;
 48  exports.MemoryStore = MemoryStore;
 49  
 50  /**
 51   * Warning message for `MemoryStore` usage in production.
 52   * @private
 53   */
 54  
 55  var warning = 'Warning: connect.session() MemoryStore is not\n'
 56    + 'designed for a production environment, as it will leak\n'
 57    + 'memory, and will not scale past a single process.';
 58  
 59  /**
 60   * Node.js 0.8+ async implementation.
 61   * @private
 62   */
 63  
 64  /* istanbul ignore next */
 65  var defer = typeof setImmediate === 'function'
 66    ? setImmediate
 67    : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
 68  
 69  /**
 70   * Setup session store with the given `options`.
 71   *
 72   * @param {Object} [options]
 73   * @param {Object} [options.cookie] Options for cookie
 74   * @param {Function} [options.genid]
 75   * @param {String} [options.name=connect.sid] Session ID cookie name
 76   * @param {Boolean} [options.proxy]
 77   * @param {Boolean} [options.resave] Resave unmodified sessions back to the store
 78   * @param {Boolean} [options.rolling] Enable/disable rolling session expiration
 79   * @param {Boolean} [options.saveUninitialized] Save uninitialized sessions to the store
 80   * @param {String|Array} [options.secret] Secret for signing session ID
 81   * @param {Object} [options.store=MemoryStore] Session store
 82   * @param {String} [options.unset]
 83   * @return {Function} middleware
 84   * @public
 85   */
 86  
 87  function session(options) {
 88    var opts = options || {}
 89  
 90    // get the cookie options
 91    var cookieOptions = opts.cookie || {}
 92  
 93    // get the session id generate function
 94    var generateId = opts.genid || generateSessionId
 95  
 96    // get the session cookie name
 97    var name = opts.name || opts.key || 'connect.sid'
 98  
 99    // get the session store
100    var store = opts.store || new MemoryStore()
101  
102    // get the trust proxy setting
103    var trustProxy = opts.proxy
104  
105    // get the resave session option
106    var resaveSession = opts.resave;
107  
108    // get the rolling session option
109    var rollingSessions = Boolean(opts.rolling)
110  
111    // get the save uninitialized session option
112    var saveUninitializedSession = opts.saveUninitialized
113  
114    // get the cookie signing secret
115    var secret = opts.secret
116  
117    if (typeof generateId !== 'function') {
118      throw new TypeError('genid option must be a function');
119    }
120  
121    if (resaveSession === undefined) {
122      deprecate('undefined resave option; provide resave option');
123      resaveSession = true;
124    }
125  
126    if (saveUninitializedSession === undefined) {
127      deprecate('undefined saveUninitialized option; provide saveUninitialized option');
128      saveUninitializedSession = true;
129    }
130  
131    if (opts.unset && opts.unset !== 'destroy' && opts.unset !== 'keep') {
132      throw new TypeError('unset option must be "destroy" or "keep"');
133    }
134  
135    // TODO: switch to "destroy" on next major
136    var unsetDestroy = opts.unset === 'destroy'
137  
138    if (Array.isArray(secret) && secret.length === 0) {
139      throw new TypeError('secret option array must contain one or more strings');
140    }
141  
142    if (secret && !Array.isArray(secret)) {
143      secret = [secret];
144    }
145  
146    if (!secret) {
147      deprecate('req.secret; provide secret option');
148    }
149  
150    // notify user that this store is not
151    // meant for a production environment
152    /* istanbul ignore next: not tested */
153    if (env === 'production' && store instanceof MemoryStore) {
154      console.warn(warning);
155    }
156  
157    // generates the new session
158    store.generate = function(req){
159      req.sessionID = generateId(req);
160      req.session = new Session(req);
161      req.session.cookie = new Cookie(cookieOptions);
162  
163      if (cookieOptions.secure === 'auto') {
164        req.session.cookie.secure = issecure(req, trustProxy);
165      }
166    };
167  
168    var storeImplementsTouch = typeof store.touch === 'function';
169  
170    // register event listeners for the store to track readiness
171    var storeReady = true
172    store.on('disconnect', function ondisconnect() {
173      storeReady = false
174    })
175    store.on('connect', function onconnect() {
176      storeReady = true
177    })
178  
179    return function session(req, res, next) {
180      // self-awareness
181      if (req.session) {
182        next()
183        return
184      }
185  
186      // Handle connection as if there is no session if
187      // the store has temporarily disconnected etc
188      if (!storeReady) {
189        debug('store is disconnected')
190        next()
191        return
192      }
193  
194      // pathname mismatch
195      var originalPath = parseUrl.original(req).pathname || '/'
196      if (originalPath.indexOf(cookieOptions.path || '/') !== 0) {
197        debug('pathname mismatch')
198        next()
199        return
200      }
201  
202      // ensure a secret is available or bail
203      if (!secret && !req.secret) {
204        next(new Error('secret option required for sessions'));
205        return;
206      }
207  
208      // backwards compatibility for signed cookies
209      // req.secret is passed from the cookie parser middleware
210      var secrets = secret || [req.secret];
211  
212      var originalHash;
213      var originalId;
214      var savedHash;
215      var touched = false
216  
217      // expose store
218      req.sessionStore = store;
219  
220      // get the session ID from the cookie
221      var cookieId = req.sessionID = getcookie(req, name, secrets);
222  
223      // set-cookie
224      onHeaders(res, function(){
225        if (!req.session) {
226          debug('no session');
227          return;
228        }
229  
230        if (!shouldSetCookie(req)) {
231          return;
232        }
233  
234        // only send secure cookies via https
235        if (req.session.cookie.secure && !issecure(req, trustProxy)) {
236          debug('not secured');
237          return;
238        }
239  
240        if (!touched) {
241          // touch session
242          req.session.touch()
243          touched = true
244        }
245  
246        // set cookie
247        try {
248          setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data)
249        } catch (err) {
250          defer(next, err)
251        }
252      });
253  
254      // proxy end() to commit the session
255      var _end = res.end;
256      var _write = res.write;
257      var ended = false;
258      res.end = function end(chunk, encoding) {
259        if (ended) {
260          return false;
261        }
262  
263        ended = true;
264  
265        var ret;
266        var sync = true;
267  
268        function writeend() {
269          if (sync) {
270            ret = _end.call(res, chunk, encoding);
271            sync = false;
272            return;
273          }
274  
275          _end.call(res);
276        }
277  
278        function writetop() {
279          if (!sync) {
280            return ret;
281          }
282  
283          if (!res._header) {
284            res._implicitHeader()
285          }
286  
287          if (chunk == null) {
288            ret = true;
289            return ret;
290          }
291  
292          var contentLength = Number(res.getHeader('Content-Length'));
293  
294          if (!isNaN(contentLength) && contentLength > 0) {
295            // measure chunk
296            chunk = !Buffer.isBuffer(chunk)
297              ? Buffer.from(chunk, encoding)
298              : chunk;
299            encoding = undefined;
300  
301            if (chunk.length !== 0) {
302              debug('split response');
303              ret = _write.call(res, chunk.slice(0, chunk.length - 1));
304              chunk = chunk.slice(chunk.length - 1, chunk.length);
305              return ret;
306            }
307          }
308  
309          ret = _write.call(res, chunk, encoding);
310          sync = false;
311  
312          return ret;
313        }
314  
315        if (shouldDestroy(req)) {
316          // destroy session
317          debug('destroying');
318          store.destroy(req.sessionID, function ondestroy(err) {
319            if (err) {
320              defer(next, err);
321            }
322  
323            debug('destroyed');
324            writeend();
325          });
326  
327          return writetop();
328        }
329  
330        // no session to save
331        if (!req.session) {
332          debug('no session');
333          return _end.call(res, chunk, encoding);
334        }
335  
336        if (!touched) {
337          // touch session
338          req.session.touch()
339          touched = true
340        }
341  
342        if (shouldSave(req)) {
343          req.session.save(function onsave(err) {
344            if (err) {
345              defer(next, err);
346            }
347  
348            writeend();
349          });
350  
351          return writetop();
352        } else if (storeImplementsTouch && shouldTouch(req)) {
353          // store implements touch method
354          debug('touching');
355          store.touch(req.sessionID, req.session, function ontouch(err) {
356            if (err) {
357              defer(next, err);
358            }
359  
360            debug('touched');
361            writeend();
362          });
363  
364          return writetop();
365        }
366  
367        return _end.call(res, chunk, encoding);
368      };
369  
370      // generate the session
371      function generate() {
372        store.generate(req);
373        originalId = req.sessionID;
374        originalHash = hash(req.session);
375        wrapmethods(req.session);
376      }
377  
378      // inflate the session
379      function inflate (req, sess) {
380        store.createSession(req, sess)
381        originalId = req.sessionID
382        originalHash = hash(sess)
383  
384        if (!resaveSession) {
385          savedHash = originalHash
386        }
387  
388        wrapmethods(req.session)
389      }
390  
391      function rewrapmethods (sess, callback) {
392        return function () {
393          if (req.session !== sess) {
394            wrapmethods(req.session)
395          }
396  
397          callback.apply(this, arguments)
398        }
399      }
400  
401      // wrap session methods
402      function wrapmethods(sess) {
403        var _reload = sess.reload
404        var _save = sess.save;
405  
406        function reload(callback) {
407          debug('reloading %s', this.id)
408          _reload.call(this, rewrapmethods(this, callback))
409        }
410  
411        function save() {
412          debug('saving %s', this.id);
413          savedHash = hash(this);
414          _save.apply(this, arguments);
415        }
416  
417        Object.defineProperty(sess, 'reload', {
418          configurable: true,
419          enumerable: false,
420          value: reload,
421          writable: true
422        })
423  
424        Object.defineProperty(sess, 'save', {
425          configurable: true,
426          enumerable: false,
427          value: save,
428          writable: true
429        });
430      }
431  
432      // check if session has been modified
433      function isModified(sess) {
434        return originalId !== sess.id || originalHash !== hash(sess);
435      }
436  
437      // check if session has been saved
438      function isSaved(sess) {
439        return originalId === sess.id && savedHash === hash(sess);
440      }
441  
442      // determine if session should be destroyed
443      function shouldDestroy(req) {
444        return req.sessionID && unsetDestroy && req.session == null;
445      }
446  
447      // determine if session should be saved to store
448      function shouldSave(req) {
449        // cannot set cookie without a session ID
450        if (typeof req.sessionID !== 'string') {
451          debug('session ignored because of bogus req.sessionID %o', req.sessionID);
452          return false;
453        }
454  
455        return !saveUninitializedSession && !savedHash && cookieId !== req.sessionID
456          ? isModified(req.session)
457          : !isSaved(req.session)
458      }
459  
460      // determine if session should be touched
461      function shouldTouch(req) {
462        // cannot set cookie without a session ID
463        if (typeof req.sessionID !== 'string') {
464          debug('session ignored because of bogus req.sessionID %o', req.sessionID);
465          return false;
466        }
467  
468        return cookieId === req.sessionID && !shouldSave(req);
469      }
470  
471      // determine if cookie should be set on response
472      function shouldSetCookie(req) {
473        // cannot set cookie without a session ID
474        if (typeof req.sessionID !== 'string') {
475          return false;
476        }
477  
478        return cookieId !== req.sessionID
479          ? saveUninitializedSession || isModified(req.session)
480          : rollingSessions || req.session.cookie.expires != null && isModified(req.session);
481      }
482  
483      // generate a session if the browser doesn't send a sessionID
484      if (!req.sessionID) {
485        debug('no SID sent, generating session');
486        generate();
487        next();
488        return;
489      }
490  
491      // generate the session object
492      debug('fetching %s', req.sessionID);
493      store.get(req.sessionID, function(err, sess){
494        // error handling
495        if (err && err.code !== 'ENOENT') {
496          debug('error %j', err);
497          next(err)
498          return
499        }
500  
501        try {
502          if (err || !sess) {
503            debug('no session found')
504            generate()
505          } else {
506            debug('session found')
507            inflate(req, sess)
508          }
509        } catch (e) {
510          next(e)
511          return
512        }
513  
514        next()
515      });
516    };
517  };
518  
519  /**
520   * Generate a session ID for a new session.
521   *
522   * @return {String}
523   * @private
524   */
525  
526  function generateSessionId(sess) {
527    return uid(24);
528  }
529  
530  /**
531   * Get the session ID cookie from request.
532   *
533   * @return {string}
534   * @private
535   */
536  
537  function getcookie(req, name, secrets) {
538    var header = req.headers.cookie;
539    var raw;
540    var val;
541  
542    // read from cookie header
543    if (header) {
544      var cookies = cookie.parse(header);
545  
546      raw = cookies[name];
547  
548      if (raw) {
549        if (raw.substr(0, 2) === 's:') {
550          val = unsigncookie(raw.slice(2), secrets);
551  
552          if (val === false) {
553            debug('cookie signature invalid');
554            val = undefined;
555          }
556        } else {
557          debug('cookie unsigned')
558        }
559      }
560    }
561  
562    // back-compat read from cookieParser() signedCookies data
563    if (!val && req.signedCookies) {
564      val = req.signedCookies[name];
565  
566      if (val) {
567        deprecate('cookie should be available in req.headers.cookie');
568      }
569    }
570  
571    // back-compat read from cookieParser() cookies data
572    if (!val && req.cookies) {
573      raw = req.cookies[name];
574  
575      if (raw) {
576        if (raw.substr(0, 2) === 's:') {
577          val = unsigncookie(raw.slice(2), secrets);
578  
579          if (val) {
580            deprecate('cookie should be available in req.headers.cookie');
581          }
582  
583          if (val === false) {
584            debug('cookie signature invalid');
585            val = undefined;
586          }
587        } else {
588          debug('cookie unsigned')
589        }
590      }
591    }
592  
593    return val;
594  }
595  
596  /**
597   * Hash the given `sess` object omitting changes to `.cookie`.
598   *
599   * @param {Object} sess
600   * @return {String}
601   * @private
602   */
603  
604  function hash(sess) {
605    // serialize
606    var str = JSON.stringify(sess, function (key, val) {
607      // ignore sess.cookie property
608      if (this === sess && key === 'cookie') {
609        return
610      }
611  
612      return val
613    })
614  
615    // hash
616    return crypto
617      .createHash('sha1')
618      .update(str, 'utf8')
619      .digest('hex')
620  }
621  
622  /**
623   * Determine if request is secure.
624   *
625   * @param {Object} req
626   * @param {Boolean} [trustProxy]
627   * @return {Boolean}
628   * @private
629   */
630  
631  function issecure(req, trustProxy) {
632    // socket is https server
633    if (req.connection && req.connection.encrypted) {
634      return true;
635    }
636  
637    // do not trust proxy
638    if (trustProxy === false) {
639      return false;
640    }
641  
642    // no explicit trust; try req.secure from express
643    if (trustProxy !== true) {
644      return req.secure === true
645    }
646  
647    // read the proto from x-forwarded-proto header
648    var header = req.headers['x-forwarded-proto'] || '';
649    var index = header.indexOf(',');
650    var proto = index !== -1
651      ? header.substr(0, index).toLowerCase().trim()
652      : header.toLowerCase().trim()
653  
654    return proto === 'https';
655  }
656  
657  /**
658   * Set cookie on response.
659   *
660   * @private
661   */
662  
663  function setcookie(res, name, val, secret, options) {
664    var signed = 's:' + signature.sign(val, secret);
665    var data = cookie.serialize(name, signed, options);
666  
667    debug('set-cookie %s', data);
668  
669    var prev = res.getHeader('Set-Cookie') || []
670    var header = Array.isArray(prev) ? prev.concat(data) : [prev, data];
671  
672    res.setHeader('Set-Cookie', header)
673  }
674  
675  /**
676   * Verify and decode the given `val` with `secrets`.
677   *
678   * @param {String} val
679   * @param {Array} secrets
680   * @returns {String|Boolean}
681   * @private
682   */
683  function unsigncookie(val, secrets) {
684    for (var i = 0; i < secrets.length; i++) {
685      var result = signature.unsign(val, secrets[i]);
686  
687      if (result !== false) {
688        return result;
689      }
690    }
691  
692    return false;
693  }