[Pkg-javascript-commits] [node-express-session] 04/12: Imported Upstream version 1.7.0

Jérémy Lal kapouer at moszumanska.debian.org
Fri Jul 25 22:27:41 UTC 2014


This is an automated email from the git hooks/post-receive script.

kapouer pushed a commit to branch master
in repository node-express-session.

commit ef7655a1e23ca6f8b47c32e24e933182d9a4e1b6
Author: Jérémy Lal <kapouer at melix.org>
Date:   Fri Jul 25 23:01:33 2014 +0200

    Imported Upstream version 1.7.0
---
 .gitignore        |    3 +
 .travis.yml       |    1 +
 History.md        |   78 ++++
 README.md         |   34 +-
 index.js          |  281 +++++++++++---
 package.json      |   22 +-
 session/memory.js |   86 ++--
 test/session.js   | 1123 +++++++++++++++++++++++++++++++++++++++++++++--------
 8 files changed, 1358 insertions(+), 270 deletions(-)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..df9af16
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+coverage
+node_modules
+npm-debug.log
diff --git a/.travis.yml b/.travis.yml
index bb47c1b..1ff243c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,3 +8,4 @@ matrix:
     - node_js: "0.11"
   fast_finish: true
 script: "npm run-script test-travis"
+after_script: "npm install coveralls at 2.10.0 && cat ./coverage/lcov.info | coveralls"
diff --git a/History.md b/History.md
index df529d2..a994dc5 100644
--- a/History.md
+++ b/History.md
@@ -1,3 +1,81 @@
+1.7.0 / 2014-07-22
+==================
+
+  * Improve session-ending error handling
+    - Errors are passed to `next(err)` instead of `console.error`
+  * deps: debug at 1.0.4
+  * deps: depd at 0.4.2
+    - Add `TRACE_DEPRECATION` environment variable
+    - Remove non-standard grey color from color output
+    - Support `--no-deprecation` argument
+    - Support `--trace-deprecation` argument
+
+1.6.5 / 2014-07-11
+==================
+
+  * Do not require `req.originalUrl`
+  * deps: debug at 1.0.3
+    - Add support for multiple wildcards in namespaces
+
+1.6.4 / 2014-07-07
+==================
+
+  * Fix blank responses for stores with synchronous operations
+
+1.6.3 / 2014-07-04
+==================
+
+  * Fix resave deprecation message
+
+1.6.2 / 2014-07-04
+==================
+
+  * Fix confusing option deprecation messages
+
+1.6.1 / 2014-06-28
+==================
+
+  * Fix saveUninitialized deprecation message
+
+1.6.0 / 2014-06-28
+==================
+
+  * Add deprecation message to undefined `resave` option
+  * Add deprecation message to undefined `saveUninitialized` option
+  * Fix `res.end` patch to return correct value
+  * Fix `res.end` patch to handle multiple `res.end` calls
+  * Reject cookies with missing signatures
+
+1.5.2 / 2014-06-26
+==================
+
+  * deps: cookie-signature at 1.0.4
+    - fix for timing attacks
+
+1.5.1 / 2014-06-21
+==================
+
+  * Move hard-to-track-down `req.secret` deprecation message
+
+1.5.0 / 2014-06-19
+==================
+
+  * Debug name is now "express-session"
+  * Deprecate integration with `cookie-parser` middleware
+  * Deprecate looking for secret in `req.secret`
+  * Directly read cookies; `cookie-parser` no longer required
+  * Directly set cookies; `res.cookie` no longer required
+  * Generate session IDs with `uid-safe`, faster and even less collisions
+
+1.4.0 / 2014-06-17
+==================
+
+  * Add `genid` option to generate custom session IDs
+  * Add `saveUninitialized` option to control saving uninitialized sessions
+  * Add `unset` option to control unsetting `req.session`
+  * Generate session IDs with `rand-token` by default; reduce collisions
+  * deps: buffer-crc32 at 0.2.3
+
 1.3.1 / 2014-06-14
 ==================
 
diff --git a/README.md b/README.md
index 2905768..dd1ba7a 100644
--- a/README.md
+++ b/README.md
@@ -2,19 +2,19 @@
 
 [![NPM Version](https://badge.fury.io/js/express-session.svg)](https://badge.fury.io/js/express-session)
 [![Build Status](https://travis-ci.org/expressjs/session.svg?branch=master)](https://travis-ci.org/expressjs/session)
+[![Coverage Status](https://img.shields.io/coveralls/expressjs/session.svg?branch=master)](https://coveralls.io/r/expressjs/session)
 
-THIS REPOSITORY NEEDS A MAINTAINER. IF YOU'RE INTERESTED IN MAINTAINING THIS REPOSITORY, PLEASE LET US KNOW!
+THIS REPOSITORY NEEDS A MAINTAINER.
+If you are interested in maintaining this module, please start contributing by making PRs and solving / discussing unsolved issues.
 
 ## API
 
 ```js
-var express      = require('express')
-var cookieParser = require('cookie-parser')
-var session      = require('express-session')
+var express = require('express')
+var session = require('express-session')
 
 var app = express()
 
-app.use(cookieParser()) // required before session.
 app.use(session({secret: 'keyboard cat'}))
 ```
 
@@ -23,9 +23,7 @@ app.use(session({secret: 'keyboard cat'}))
 
 Setup session store with the given `options`.
 
-Session data is _not_ saved in the cookie itself, however
-cookies are used, so we must use the [cookie-parser](https://github.com/expressjs/cookie-parser)
-middleware _before_ `session()`.
+Session data is _not_ saved in the cookie itself, just the session ID.
 
 #### Options
 
@@ -34,10 +32,27 @@ middleware _before_ `session()`.
   - `secret` - session cookie is signed with this secret to prevent tampering.
   - `cookie` - session cookie settings.
     - (default: `{ path: '/', httpOnly: true, secure: false, maxAge: null }`)
+  - `genid` - function to call to generate a new session ID. (default: uses `uid2` library)
   - `rolling` - forces a cookie set on every response. This resets the expiration date. (default: `false`)
   - `resave` - forces session to be saved even when unmodified. (default: `true`)
   - `proxy` - trust the reverse proxy when setting secure cookies (via "x-forwarded-proto" header). When set to `true`, the "x-forwarded-proto" header will be used. When set to `false`, all headers are ignored. When left unset, will use the "trust proxy" setting from express. (default: `undefined`)
+  - `saveUninitialized` - forces a session that is "uninitialized" to be saved to the store. A session is uninitialized when it is new but not modified. This is useful for implementing login sessions, reducing server storage usage, or complying with laws that require permission before setting a cookie. (default: `true`)
+  - `unset` - controls result of unsetting `req.session` (through `delete`, setting to `null`, etc.). This can be "keep" to keep the session in the store but ignore modifications or "destroy" to destroy the stored session. (default: `'keep'`)
 
+#### options.genid
+
+Generate a custom session ID for new sessions. Provide a function that returns a string that will be used as a session ID. The function is given `req` as the first argument if you want to use some value attached to `req` when generating the ID.
+
+**NOTE** be careful you generate unique IDs so your sessions do not conflict.
+
+```js
+app.use(session({
+  genid: function(req) {
+    return genuuid(); // use UUIDs for session IDs
+  },
+  secret: 'keyboard cat'
+}))
+```
 
 #### Cookie options
 
@@ -47,7 +62,6 @@ If `secure` is set, and you access your site over HTTP, the cookie will not be s
 ```js
 var app = express()
 app.set('trust proxy', 1) // trust first proxy
-app.use(cookieParser())
 app.use(session({
     secret: 'keyboard cat'
   , cookie: { secure: true }
@@ -68,7 +82,6 @@ if (app.get('env') === 'production') {
   sess.cookie.secure = true // serve secure cookies
 }
 
-app.use(cookieParser())
 app.use(session(sess))
 ```
 
@@ -83,7 +96,6 @@ which is (generally) serialized as JSON by the store, so nested objects
 are typically fine. For example below is a user-specific view counter:
 
 ```js
-app.use(cookieParser())
 app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
 
 app.use(function(req, res, next) {
diff --git a/index.js b/index.js
index d48bcd1..2b886ae 100644
--- a/index.js
+++ b/index.js
@@ -9,12 +9,14 @@
  * Module dependencies.
  */
 
-var uid = require('uid2')
+var cookie = require('cookie');
+var debug = require('debug')('express-session');
+var deprecate = require('depd')('express-session');
+var uid = require('uid-safe').sync
   , onHeaders = require('on-headers')
   , crc32 = require('buffer-crc32')
   , parse = require('url').parse
   , signature = require('cookie-signature')
-  , debug = require('debug')('session')
 
 var Session = require('./session/session')
   , MemoryStore = require('./session/memory')
@@ -49,6 +51,15 @@ var warning = 'Warning: connect.session() MemoryStore is not\n'
   + 'memory, and will not scale past a single process.';
 
 /**
+ * Node.js 0.8+ async implementation.
+ */
+
+/* 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`.
  *
  * See README.md for documentation of options and formatting.
@@ -71,11 +82,31 @@ function session(options){
     , trustProxy = options.proxy
     , storeReady = true
     , rollingSessions = options.rolling || false;
+  var resaveSession = options.resave;
+  var saveUninitializedSession = options.saveUninitialized;
+
+  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 default to false on next major
-  var resaveSession = options.resave === undefined
-    ? true
-    : options.resave;
+  // TODO: switch to "destroy" on next major
+  var unsetDestroy = options.unset === 'destroy';
 
   // notify user that this store is not
   // meant for a production environment
@@ -85,7 +116,7 @@ function session(options){
 
   // generates the new session
   store.generate = function(req){
-    req.sessionID = uid(24);
+    req.sessionID = generateId(req);
     req.session = new Session(req);
     req.session.cookie = new Cookie(cookie);
   };
@@ -93,6 +124,10 @@ function session(options){
   store.on('disconnect', function(){ storeReady = false; });
   store.on('connect', function(){ storeReady = true; });
 
+  if (!options.secret) {
+    deprecate('req.secret; provide secret option');
+  }
+
   return function session(req, res, next) {
     // self-awareness
     if (req.session) return next();
@@ -102,7 +137,7 @@ function session(options){
     if (!storeReady) return debug('store is disconnected'), next();
 
     // pathname mismatch
-    var originalPath = parse(req.originalUrl).pathname;
+    var originalPath = parse(req.originalUrl || req.url).pathname;
     if (0 != originalPath.indexOf(cookie.path || '/')) return next();
 
     // backwards compatibility for signed cookies
@@ -110,7 +145,7 @@ function session(options){
     var secret = options.secret || req.secret;
 
     // ensure secret is available or bail
-    if (!secret) throw new Error('`secret` option required for sessions');
+    if (!secret) next(new Error('`secret` option required for sessions'));
 
     var originalHash
       , originalId;
@@ -118,17 +153,8 @@ function session(options){
     // expose store
     req.sessionStore = store;
 
-    // grab the session cookie value and check the signature
-    var rawCookie = req.cookies[name];
-
-    // get signedCookies for backwards compat with signed cookies
-    var unsignedCookie = req.signedCookies[name];
-
-    if (!unsignedCookie && rawCookie) {
-      unsignedCookie = (0 == rawCookie.indexOf('s:'))
-        ? signature.unsign(rawCookie.slice(2), secret)
-        : rawCookie;
-    }
+    // get the session ID from the cookie
+    var cookieId = req.sessionID = getcookie(req, name, secret);
 
     // set-cookie
     onHeaders(res, function(){
@@ -145,52 +171,99 @@ function session(options){
         return;
       }
 
-      var isNew = unsignedCookie != req.sessionID;
+      if (!shouldSetCookie(req)) {
+        return;
+      }
 
-      // in case of rolling session, always reset the cookie
-      if (!rollingSessions) {
+      setcookie(res, name, req.sessionID, secret, cookie.data);
+    });
+
+    // proxy end() to commit the session
+    var end = res.end;
+    var ended = false;
+    res.end = function(chunk, encoding){
+      if (ended) {
+        return false;
+      }
 
-        // browser-session length cookie
-        if (null == cookie.expires) {
-          if (!isNew) {
-            debug('already set browser-session cookie');
-            return
+      var ret;
+      var sync = true;
+
+      if (chunk === undefined) {
+        chunk = '';
+      }
+
+      ended = true;
+
+      if (shouldDestroy(req)) {
+        // destroy session
+        debug('destroying');
+        store.destroy(req.sessionID, function ondestroy(err) {
+          if (err) {
+            defer(next, err);
+          }
+
+          debug('destroyed');
+
+          if (sync) {
+            ret = end.call(res, chunk, encoding);
+            sync = false;
+            return;
           }
-        // compare hashes and ids
-        } else if (!isModified(req.session)) {
-          debug('unmodified session');
-          return
+
+          end.call(res);
+        });
+
+        if (sync) {
+          ret = res.write(chunk, encoding);
+          sync = false;
         }
 
+        return ret;
       }
 
-      var val = 's:' + signature.sign(req.sessionID, secret);
-      debug('set-cookie %s', val);
-      res.cookie(name, val, cookie.data);
-    });
+      // no session to save
+      if (!req.session) {
+        debug('no session');
+        return end.call(res, chunk, encoding);
+      }
 
-    // proxy end() to commit the session
-    var end = res.end;
-    res.end = function(data, encoding){
-      res.end = end;
-      if (!req.session) return res.end(data, encoding);
       req.session.resetMaxAge();
 
-      if (resaveSession || isModified(req.session)) {
+      if (shouldSave(req)) {
         debug('saving');
-        return req.session.save(function(err){
-          if (err) console.error(err.stack);
+        req.session.save(function onsave(err) {
+          if (err) {
+            defer(next, err);
+          }
+
           debug('saved');
-          res.end(data, encoding);
+
+          if (sync) {
+            ret = end.call(res, chunk, encoding);
+            sync = false;
+            return;
+          }
+
+          end.call(res);
         });
+
+        if (sync) {
+          ret = res.write(chunk, encoding);
+          sync = false;
+        }
+
+        return ret;
       }
 
-      res.end(data, encoding);
+      return end.call(res, chunk, encoding);
     };
 
     // generate the session
     function generate() {
       store.generate(req);
+      originalId = req.sessionID;
+      originalHash = hash(req.session);
     }
 
     // check if session has been modified
@@ -198,8 +271,29 @@ function session(options){
       return originalHash != hash(sess) || originalId != sess.id;
     }
 
-    // get the sessionID from the cookie
-    req.sessionID = unsignedCookie;
+    // 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) {
+      return cookieId != req.sessionID
+        ? saveUninitializedSession || isModified(req.session)
+        : resaveSession || isModified(req.session);
+    }
+
+    // determine if cookie should be set on response
+    function shouldSetCookie(req) {
+      // in case of rolling session, always reset the cookie
+      if (rollingSessions) {
+        return true;
+      }
+
+      return cookieId != req.sessionID
+        ? saveUninitializedSession || isModified(req.session)
+        : req.session.cookie.expires != null && isModified(req.session);
+    }
 
     // generate a session if the browser doesn't send a sessionID
     if (!req.sessionID) {
@@ -239,6 +333,83 @@ function session(options){
 };
 
 /**
+ * Generate a session ID for a new session.
+ *
+ * @return {String}
+ * @api private
+ */
+
+function generateSessionId(sess) {
+  return uid(24);
+}
+
+/**
+ * Get the session ID cookie from request.
+ *
+ * @return {string}
+ * @api private
+ */
+
+function getcookie(req, name, secret) {
+  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 = signature.unsign(raw.slice(2), secret);
+
+        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 = signature.unsign(raw.slice(2), secret);
+
+        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
@@ -289,3 +460,17 @@ function issecure(req, trustProxy) {
 
   return proto === 'https';
 }
+
+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)
+    : Array.isArray(data) ? [prev].concat(data)
+    : [prev, data];
+
+  res.setHeader('set-cookie', header)
+}
diff --git a/package.json b/package.json
index df44c60..520fb84 100644
--- a/package.json
+++ b/package.json
@@ -1,23 +1,29 @@
 {
   "name": "express-session",
-  "version": "1.3.1",
+  "version": "1.7.0",
   "description": "Simple session middleware for Express",
   "author": "TJ Holowaychuk <tj at vision-media.ca> (http://tjholowaychuk.com)",
+  "contributors": [
+    "Douglas Christopher Wilson <doug at somethingdoug.com>",
+    "Joe Wagner <njwjs722 at gmail.com>"
+  ],
   "repository": "expressjs/session",
   "license": "MIT",
   "dependencies": {
-    "buffer-crc32": "0.2.1",
+    "buffer-crc32": "0.2.3",
     "cookie": "0.1.2",
-    "cookie-signature": "1.0.3",
-    "debug": "1.0.2",
+    "cookie-signature": "1.0.4",
+    "debug": "1.0.4",
+    "depd": "0.4.2",
     "on-headers": "0.0.0",
-    "uid2": "0.0.3",
+    "uid-safe": "1.0.1",
     "utils-merge": "1.0.0"
   },
   "devDependencies": {
-    "cookie-parser": "1.1.0",
-    "istanbul": "0.2.10",
-    "express": "~4.4.0",
+    "after": "0.8.1",
+    "cookie-parser": "1.3.2",
+    "istanbul": "0.3.0",
+    "express": "~4.6.1",
     "mocha": "~1.20.1",
     "should": "~4.0.4",
     "supertest": "~0.13.0"
diff --git a/session/memory.js b/session/memory.js
index 9720b06..4efe99d 100644
--- a/session/memory.js
+++ b/session/memory.js
@@ -16,9 +16,10 @@ var Store = require('./store');
  * Shim setImmediate for node.js < 0.10
  */
 
-var asyncTick = typeof setImmediate === 'function'
+/* istanbul ignore next */
+var defer = typeof setImmediate === 'function'
   ? setImmediate
-  : process.nextTick;
+  : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) }
 
 /**
  * Initialize a new `MemoryStore`.
@@ -27,7 +28,7 @@ var asyncTick = typeof setImmediate === 'function'
  */
 
 var MemoryStore = module.exports = function MemoryStore() {
-  this.sessions = {};
+  this.sessions = Object.create(null);
 };
 
 /**
@@ -46,23 +47,25 @@ MemoryStore.prototype.__proto__ = Store.prototype;
 
 MemoryStore.prototype.get = function(sid, fn){
   var self = this;
-  asyncTick(function(){
-    var expires
-      , sess = self.sessions[sid];
-    if (sess) {
-      sess = JSON.parse(sess);
-      expires = 'string' == typeof sess.cookie.expires
-        ? new Date(sess.cookie.expires)
-        : sess.cookie.expires;
-      if (!expires || new Date < expires) {
-        fn(null, sess);
-      } else {
-        self.destroy(sid, fn);
-      }
-    } else {
-      fn();
-    }
-  });
+  var sess = self.sessions[sid];
+
+  if (!sess) {
+    return defer(fn);
+  }
+
+  // parse
+  sess = JSON.parse(sess);
+
+  var expires = typeof sess.cookie.expires === 'string'
+    ? new Date(sess.cookie.expires)
+    : sess.cookie.expires;
+
+  // destroy expired session
+  if (expires && expires <= Date.now()) {
+    return self.destroy(sid, fn);
+  }
+
+  defer(fn, null, sess);
 };
 
 /**
@@ -75,11 +78,8 @@ MemoryStore.prototype.get = function(sid, fn){
  */
 
 MemoryStore.prototype.set = function(sid, sess, fn){
-  var self = this;
-  asyncTick(function(){
-    self.sessions[sid] = JSON.stringify(sess);
-    fn && fn();
-  });
+  this.sessions[sid] = JSON.stringify(sess);
+  fn && defer(fn);
 };
 
 /**
@@ -90,11 +90,8 @@ MemoryStore.prototype.set = function(sid, sess, fn){
  */
 
 MemoryStore.prototype.destroy = function(sid, fn){
-  var self = this;
-  asyncTick(function(){
-    delete self.sessions[sid];
-    fn && fn();
-  });
+  delete this.sessions[sid];
+  fn && defer(fn);
 };
 
 /**
@@ -105,12 +102,28 @@ MemoryStore.prototype.destroy = function(sid, fn){
  */
 
 MemoryStore.prototype.all = function(fn){
-  var arr = []
-    , keys = Object.keys(this.sessions);
+  var keys = Object.keys(this.sessions);
+  var now = Date.now();
+  var obj = Object.create(null);
+  var sess;
+  var sid;
+
   for (var i = 0, len = keys.length; i < len; ++i) {
-    arr.push(this.sessions[keys[i]]);
+    sid = keys[i];
+
+    // parse
+    sess = JSON.parse(this.sessions[sid]);
+
+    expires = typeof sess.cookie.expires === 'string'
+      ? new Date(sess.cookie.expires)
+      : sess.cookie.expires;
+
+    if (!expires || expires > now) {
+      obj[sid] = sess;
+    }
   }
-  fn(null, arr);
+
+  fn && defer(fn, null, obj);
 };
 
 /**
@@ -122,7 +135,7 @@ MemoryStore.prototype.all = function(fn){
 
 MemoryStore.prototype.clear = function(fn){
   this.sessions = {};
-  fn && fn();
+  fn && defer(fn);
 };
 
 /**
@@ -133,5 +146,6 @@ MemoryStore.prototype.clear = function(fn){
  */
 
 MemoryStore.prototype.length = function(fn){
-  fn(null, Object.keys(this.sessions).length);
+  var len = Object.keys(this.sessions).length;
+  defer(fn, null, len);
 };
diff --git a/test/session.js b/test/session.js
index 54622bf..20fc8ca 100644
--- a/test/session.js
+++ b/test/session.js
@@ -1,4 +1,7 @@
 
+process.env.NO_DEPRECATION = 'express-session';
+
+var after = require('after')
 var express = require('express')
   , assert = require('assert')
   , request = require('supertest')
@@ -6,18 +9,10 @@ var express = require('express')
   , cookieParser = require('cookie-parser')
   , session = require('../')
   , Cookie = require('../session/cookie')
+var http = require('http');
 
 var min = 60 * 1000;
 
-function respond(req, res) {
-  res.end();
-}
-
-var app = express()
-  .use(cookieParser())
-  .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
-  .use(respond);
-
 describe('session()', function(){
   it('should export constructors', function(){
     session.Session.should.be.a.Function;
@@ -25,15 +20,354 @@ describe('session()', function(){
     session.MemoryStore.should.be.a.Function;
   })
 
+  it('should do nothing if req.session exists', function(done){
+    var app = express()
+      .use(function(req, res, next){ req.session = {}; next(); })
+      .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
+      .use(end);
+
+      request(app)
+      .get('/')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        should(cookie(res)).be.empty;
+        done();
+      });
+  })
+
+  it('should error without secret', function(done){
+    request(createServer({ secret: undefined }))
+    .get('/')
+    .expect(500, /secret.*required/, done)
+  })
+
+  it('should get secret from req.secret', function(done){
+    var app = express()
+      .use(function(req, res, next){ req.secret = 'keyboard cat'; next(); })
+      .use(session({ cookie: { maxAge: min }}))
+      .use(end);
+    app.set('env', 'test');
+
+      request(app)
+      .get('/')
+      .expect(200, '', done);
+  })
+
+  it('should create a new session', function (done) {
+    var store = new session.MemoryStore()
+    var server = createServer({ store: store }, function (req, res) {
+      req.session.active = true
+      res.end('session active')
+    });
+
+    request(server)
+    .get('/')
+    .expect(200, 'session active', function (err, res) {
+      if (err) return done(err)
+      should(sid(res)).not.be.empty
+      store.length(function (err, len) {
+        if (err) return done(err)
+        len.should.equal(1)
+        done()
+      })
+    })
+  })
+
+  it('should load session from cookie sid', function (done) {
+    var count = 0
+    var server = createServer(null, function (req, res) {
+      req.session.num = req.session.num || ++count
+      res.end('session ' + req.session.num)
+    });
+
+    request(server)
+    .get('/')
+    .expect(200, 'session 1', function (err, res) {
+      if (err) return done(err)
+      should(sid(res)).not.be.empty
+      request(server)
+      .get('/')
+      .set('Cookie', cookie(res))
+      .expect(200, 'session 1', done)
+    })
+  })
+
+  it('should pass session fetch error', function (done) {
+    var store = new session.MemoryStore()
+    var server = createServer({ store: store }, function (req, res) {
+      res.end('hello, world')
+    })
+
+    store.get = function destroy(sid, callback) {
+      callback(new Error('boom!'))
+    }
+
+    request(server)
+    .get('/')
+    .expect(200, 'hello, world', function (err, res) {
+      if (err) return done(err)
+      should(sid(res)).not.be.empty
+      request(server)
+      .get('/')
+      .set('Cookie', cookie(res))
+      .expect(500, 'boom!', done)
+    })
+  })
+
+  it('should treat ENOENT session fetch error as not found', function (done) {
+    var count = 0
+    var store = new session.MemoryStore()
+    var server = createServer({ store: store }, function (req, res) {
+      req.session.num = req.session.num || ++count
+      res.end('session ' + req.session.num)
+    })
+
+    store.get = function destroy(sid, callback) {
+      var err = new Error('boom!')
+      err.code = 'ENOENT'
+      callback(err)
+    }
+
+    request(server)
+    .get('/')
+    .expect(200, 'session 1', function (err, res) {
+      if (err) return done(err)
+      should(sid(res)).not.be.empty
+      request(server)
+      .get('/')
+      .set('Cookie', cookie(res))
+      .expect(200, 'session 2', done)
+    })
+  })
+
+  it('should create multiple sessions', function (done) {
+    var cb = after(2, check)
+    var count = 0
+    var store = new session.MemoryStore()
+    var server = createServer({ store: store }, function (req, res) {
+      var isnew = req.session.num === undefined
+      req.session.num = req.session.num || ++count
+      res.end('session ' + (isnew ? 'created' : 'updated'))
+    });
+
+    function check(err) {
+      if (err) return done(err)
+      store.all(function (err, sess) {
+        if (err) return done(err)
+        Object.keys(sess).should.have.length(2)
+        done()
+      })
+    }
+
+    request(server)
+    .get('/')
+    .expect(200, 'session created', cb)
+
+    request(server)
+    .get('/')
+    .expect(200, 'session created', cb)
+  })
+
+  it('should handle multiple res.end calls', function(done){
+    var app = express()
+      .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
+      .use(function(req, res){
+        res.setHeader('Content-Type', 'text/plain');
+        res.end('Hello, world!');
+        res.end();
+      });
+    app.set('env', 'test');
+
+    request(app)
+    .get('/')
+    .expect('Content-Type', 'text/plain')
+    .expect(200, 'Hello, world!', done);
+  })
+
+  describe('when sid not in store', function () {
+    it('should create a new session', function (done) {
+      var count = 0
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store }, function (req, res) {
+        req.session.num = req.session.num || ++count
+        res.end('session ' + req.session.num)
+      });
+
+      request(server)
+      .get('/')
+      .expect(200, 'session 1', function (err, res) {
+        if (err) return done(err)
+        should(sid(res)).not.be.empty
+        store.clear(function (err) {
+          if (err) return done(err)
+          request(server)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, 'session 2', done)
+        })
+      })
+    })
+
+    it('should have a new sid', function (done) {
+      var count = 0
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store }, function (req, res) {
+        req.session.num = req.session.num || ++count
+        res.end('session ' + req.session.num)
+      });
+
+      request(server)
+      .get('/')
+      .expect(200, 'session 1', function (err, res) {
+        if (err) return done(err)
+        var val = sid(res)
+        should(val).not.be.empty
+        store.clear(function (err) {
+          if (err) return done(err)
+          request(server)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, 'session 2', function (err, res) {
+            if (err) return done(err)
+            should(sid(res)).not.be.empty
+            should(sid(res)).not.equal(val)
+            done()
+          })
+        })
+      })
+    })
+  })
+
+  describe('when sid not properly signed', function () {
+    it('should generate new session', function (done) {
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, key: 'sessid' }, function (req, res) {
+        var isnew = req.session.active === undefined
+        req.session.active = true
+        res.end('session ' + (isnew ? 'created' : 'read'))
+      })
+
+      request(server)
+      .get('/')
+      .expect(200, 'session created', function (err, res) {
+        if (err) return done(err)
+        var val = sid(res)
+        should(val).not.be.empty
+        request(server)
+        .get('/')
+        .set('Cookie', 'sessid=' + val)
+        .expect(200, 'session created', done)
+      })
+    })
+
+    it('should not attempt fetch from store', function (done) {
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, key: 'sessid' }, function (req, res) {
+        var isnew = req.session.active === undefined
+        req.session.active = true
+        res.end('session ' + (isnew ? 'created' : 'read'))
+      })
+
+      request(server)
+      .get('/')
+      .expect(200, 'session created', function (err, res) {
+        if (err) return done(err)
+        var val = cookie(res).replace(/...\./, '.')
+
+        should(val).not.be.empty
+        request(server)
+        .get('/')
+        .set('Cookie', val)
+        .expect(200, 'session created', done)
+      })
+    })
+  })
+
+  describe('when session expired in store', function () {
+    it('should create a new session', function (done) {
+      var count = 0
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, cookie: { maxAge: 5 } }, function (req, res) {
+        req.session.num = req.session.num || ++count
+        res.end('session ' + req.session.num)
+      });
+
+      request(server)
+      .get('/')
+      .expect(200, 'session 1', function (err, res) {
+        if (err) return done(err)
+        should(sid(res)).not.be.empty
+        setTimeout(function () {
+          request(server)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, 'session 2', done)
+        }, 10)
+      })
+    })
+
+    it('should have a new sid', function (done) {
+      var count = 0
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, cookie: { maxAge: 5 } }, function (req, res) {
+        req.session.num = req.session.num || ++count
+        res.end('session ' + req.session.num)
+      });
+
+      request(server)
+      .get('/')
+      .expect(200, 'session 1', function (err, res) {
+        if (err) return done(err)
+        var val = sid(res)
+        should(val).not.be.empty
+        setTimeout(function () {
+          request(server)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, 'session 2', function (err, res) {
+            if (err) return done(err)
+            should(sid(res)).not.be.empty
+            should(sid(res)).not.equal(val)
+            done()
+          })
+        }, 10)
+      })
+    })
+
+    it('should not exist in store', function (done) {
+      var count = 0
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, cookie: { maxAge: 5 } }, function (req, res) {
+        req.session.num = req.session.num || ++count
+        res.end('session ' + req.session.num)
+      });
+
+      request(server)
+      .get('/')
+      .expect(200, 'session 1', function (err, res) {
+        if (err) return done(err)
+        var val = sid(res)
+        should(val).not.be.empty
+        setTimeout(function () {
+          store.all(function (err, sess) {
+            if (err) return done(err)
+            Object.keys(sess).should.have.length(0)
+            done()
+          })
+        }, 10)
+      })
+    })
+  })
+
   describe('proxy option', function(){
     describe('when enabled', function(){
-      it('should trust X-Forwarded-Proto when string', function(done){
-        var app = express()
-          .use(cookieParser())
-          .use(session({ secret: 'keyboard cat', proxy: true, cookie: { secure: true, maxAge: 5 }}))
-          .use(respond);
+      var server
+      before(function () {
+        server = createServer({ proxy: true, cookie: { secure: true, maxAge: 5 }})
+      })
 
-        request(app)
+      it('should trust X-Forwarded-Proto when string', function(done){
+        request(server)
         .get('/')
         .set('X-Forwarded-Proto', 'https')
         .expect(200, function(err, res){
@@ -44,12 +378,7 @@ describe('session()', function(){
       })
 
       it('should trust X-Forwarded-Proto when comma-separated list', function(done){
-        var app = express()
-          .use(cookieParser())
-          .use(session({ secret: 'keyboard cat', proxy: true, cookie: { secure: true, maxAge: 5 }}))
-          .use(respond);
-
-        request(app)
+        request(server)
         .get('/')
         .set('X-Forwarded-Proto', 'https,http')
         .expect(200, function(err, res){
@@ -60,12 +389,7 @@ describe('session()', function(){
       })
 
       it('should work when no header', function(done){
-        var app = express()
-          .use(cookieParser())
-          .use(session({ secret: 'keyboard cat', proxy: true, cookie: { secure: true, maxAge: 5 }}))
-          .use(respond);
-
-        request(app)
+        request(server)
         .get('/')
         .expect(200, function(err, res){
           if (err) return done(err);
@@ -76,13 +400,13 @@ describe('session()', function(){
     })
 
     describe('when disabled', function(){
-      it('should not trust X-Forwarded-Proto', function(done){
-        var app = express()
-          .use(cookieParser())
-          .use(session({ secret: 'keyboard cat', proxy: false, cookie: { secure: true, maxAge: min }}))
-          .use(respond);
+      var server
+      before(function () {
+        server = createServer({ proxy: false, cookie: { secure: true, maxAge: 5 }})
+      })
 
-        request(app)
+      it('should not trust X-Forwarded-Proto', function(done){
+        request(server)
         .get('/')
         .set('X-Forwarded-Proto', 'https')
         .expect(200, function(err, res){
@@ -94,7 +418,6 @@ describe('session()', function(){
 
       it('should ignore req.secure from express', function(done){
         var app = express()
-          .use(cookieParser())
           .use(session({ secret: 'keyboard cat', proxy: false, cookie: { secure: true, maxAge: min }}))
           .use(function(req, res) { res.json(req.secure); });
         app.enable('trust proxy');
@@ -111,13 +434,13 @@ describe('session()', function(){
     })
 
     describe('when unspecified', function(){
-      it('should not trust X-Forwarded-Proto', function(done){
-        var app = express()
-          .use(cookieParser())
-          .use(session({ secret: 'keyboard cat', cookie: { secure: true, maxAge: min }}))
-          .use(respond);
+      var server
+      before(function () {
+        server = createServer({ cookie: { secure: true, maxAge: 5 }})
+      })
 
-        request(app)
+      it('should not trust X-Forwarded-Proto', function(done){
+        request(server)
         .get('/')
         .set('X-Forwarded-Proto', 'https')
         .expect(200, function(err, res){
@@ -129,7 +452,6 @@ describe('session()', function(){
 
       it('should use req.secure from express', function(done){
         var app = express()
-          .use(cookieParser())
           .use(session({ secret: 'keyboard cat', cookie: { secure: true, maxAge: min }}))
           .use(function(req, res) { res.json(req.secure); });
         app.enable('trust proxy');
@@ -146,9 +468,61 @@ describe('session()', function(){
     })
   })
 
+  describe('genid option', function(){
+    it('should reject non-function values', function(){
+      session.bind(null, { genid: 'bogus!' }).should.throw(/genid.*must/);
+    });
+
+    it('should provide default generator', function(done){
+      request(createServer())
+      .get('/')
+      .expect(200, function (err, res) {
+        if (err) return done(err)
+        should(sid(res)).not.be.empty
+        done()
+      })
+    });
+
+    it('should allow custom function', function(done){
+      function genid() { return 'apple' }
+
+      request(createServer({ genid: genid }))
+      .get('/')
+      .expect(200, function (err, res) {
+        if (err) return done(err)
+        should(sid(res)).equal('apple')
+        done()
+      })
+    });
+
+    it('should encode unsafe chars', function(done){
+      function genid() { return '%' }
+
+      request(createServer({ genid: genid }))
+      .get('/')
+      .expect(200, function (err, res) {
+        if (err) return done(err)
+        should(sid(res)).equal('%25')
+        done()
+      })
+    });
+
+    it('should provide req argument', function(done){
+      function genid(req) { return req.url }
+
+      request(createServer({ genid: genid }))
+      .get('/foo')
+      .expect(200, function (err, res) {
+        if (err) return done(err)
+        should(sid(res)).equal('%2Ffoo')
+        done()
+      })
+    });
+  });
+
   describe('key option', function(){
     it('should default to "connect.sid"', function(done){
-      request(app)
+      request(createServer())
       .get('/')
       .end(function(err, res){
         res.headers['set-cookie'].should.have.length(1);
@@ -158,12 +532,7 @@ describe('session()', function(){
     })
 
     it('should allow overriding', function(done){
-      var app = express()
-        .use(cookieParser())
-        .use(session({ secret: 'keyboard cat', key: 'sid', cookie: { maxAge: min }}))
-        .use(respond);
-
-      request(app)
+      request(createServer({ key: 'sid' }))
       .get('/')
       .end(function(err, res){
         res.headers['set-cookie'].should.have.length(1);
@@ -173,11 +542,62 @@ describe('session()', function(){
     })
   })
 
+  describe('rolling option', function(){
+    it('should default to false', function(done){
+      var app = express();
+      app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}));
+      app.use(function(req, res, next){
+        var save = req.session.save;
+        req.session.user = 'bob';
+        res.end();
+      });
+
+      request(app)
+      .get('/')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        should(cookie(res)).not.be.empty;
+        request(app)
+        .get('/')
+        .set('Cookie', cookie(res))
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).be.empty;
+          done();
+        });
+      });
+    });
+
+    it('should force cookie on unmodified session', function(done){
+      var app = express();
+      app.use(session({ rolling: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
+      app.use(function(req, res, next){
+        var save = req.session.save;
+        req.session.user = 'bob';
+        res.end();
+      });
+
+      request(app)
+      .get('/')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        should(cookie(res)).not.be.empty;
+        request(app)
+        .get('/')
+        .set('Cookie', cookie(res))
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).not.be.empty;
+          done();
+        });
+      });
+    });
+  });
+
   describe('resave option', function(){
     it('should default to true', function(done){
       var count = 0;
       var app = express();
-      app.use(cookieParser());
       app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}));
       app.use(function(req, res, next){
         var save = req.session.save;
@@ -197,7 +617,7 @@ describe('session()', function(){
         if (err) return done(err);
         request(app)
         .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
+        .set('Cookie', cookie(res))
         .expect('x-count', '2')
         .expect(200, done);
       });
@@ -206,7 +626,6 @@ describe('session()', function(){
     it('should force save on unmodified session', function(done){
       var count = 0;
       var app = express();
-      app.use(cookieParser());
       app.use(session({ resave: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
       app.use(function(req, res, next){
         var save = req.session.save;
@@ -226,7 +645,7 @@ describe('session()', function(){
         if (err) return done(err);
         request(app)
         .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
+        .set('Cookie', cookie(res))
         .expect('x-count', '2')
         .expect(200, done);
       });
@@ -235,7 +654,6 @@ describe('session()', function(){
     it('should prevent save on unmodified session', function(done){
       var count = 0;
       var app = express();
-      app.use(cookieParser());
       app.use(session({ resave: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
       app.use(function(req, res, next){
         var save = req.session.save;
@@ -255,7 +673,7 @@ describe('session()', function(){
         if (err) return done(err);
         request(app)
         .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
+        .set('Cookie', cookie(res))
         .expect('x-count', '1')
         .expect(200, done);
       });
@@ -264,7 +682,6 @@ describe('session()', function(){
     it('should still save modified session', function(done){
       var count = 0;
       var app = express();
-      app.use(cookieParser());
       app.use(session({ resave: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
       app.use(function(req, res, next){
         var save = req.session.save;
@@ -285,117 +702,269 @@ describe('session()', function(){
         if (err) return done(err);
         request(app)
         .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
+        .set('Cookie', cookie(res))
         .expect('x-count', '2')
         .expect(200, done);
       });
     });
   });
 
-  it('should retain the sid', function(done){
-    var n = 0;
+  describe('saveUninitialized option', function(){
+    it('should default to true', function(done){
+      var count = 0;
+      var app = express();
+      app.use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}));
+      app.use(function(req, res, next){
+        var save = req.session.save;
+        res.setHeader('x-count', count);
+        req.session.save = function(fn){
+          res.setHeader('x-count', ++count);
+          return save.call(this, fn);
+        };
+        res.end();
+      });
 
-    var app = express()
-      .use(cookieParser())
-      .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
-      .use(function(req, res){
-        req.session.count = ++n;
+      request(app)
+      .get('/')
+      .expect('x-count', '1')
+      .expect('set-cookie', /connect\.sid=/)
+      .expect(200, done);
+    });
+
+    it('should force save of uninitialized session', function(done){
+      var count = 0;
+      var app = express();
+      app.use(session({ saveUninitialized: true, secret: 'keyboard cat', cookie: { maxAge: min }}));
+      app.use(function(req, res, next){
+        var save = req.session.save;
+        res.setHeader('x-count', count);
+        req.session.save = function(fn){
+          res.setHeader('x-count', ++count);
+          return save.call(this, fn);
+        };
         res.end();
-      })
+      });
 
-    request(app)
-    .get('/')
-    .end(function(err, res){
+      request(app)
+      .get('/')
+      .expect('x-count', '1')
+      .expect('set-cookie', /connect\.sid=/)
+      .expect(200, done);
+    });
+
+    it('should prevent save of uninitialized session', function(done){
+      var count = 0;
+      var app = express();
+      app.use(session({ saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
+      app.use(function(req, res, next){
+        var save = req.session.save;
+        res.setHeader('x-count', count);
+        req.session.save = function(fn){
+          res.setHeader('x-count', ++count);
+          return save.call(this, fn);
+        };
+        res.end();
+      });
 
-      var id = sid(res);
       request(app)
       .get('/')
-      .set('Cookie', 'connect.sid=' + id)
-      .end(function(err, res){
-        sid(res).should.equal(id);
+      .expect('x-count', '0')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        should(cookie(res)).be.empty;
         done();
       });
     });
-  })
 
-  describe('when an invalid sid is given', function(){
-    it('should generate a new one', function(done){
+    it('should still save modified session', function(done){
+      var count = 0;
+      var app = express();
+      app.use(session({ saveUninitialized: false, secret: 'keyboard cat', cookie: { maxAge: min }}));
+      app.use(function(req, res, next){
+        var save = req.session.save;
+        res.setHeader('x-count', count);
+        req.session.count = count;
+        req.session.user = 'bob';
+        req.session.save = function(fn){
+          res.setHeader('x-count', ++count);
+          return save.call(this, fn);
+        };
+        res.end();
+      });
+
       request(app)
       .get('/')
-      .set('Cookie', 'connect.sid=foobarbaz')
-      .end(function(err, res){
-        sid(res).should.not.equal('foobarbaz');
-        done();
-      });
-    })
-  })
+      .expect('x-count', '1')
+      .expect('set-cookie', /connect\.sid=/)
+      .expect(200, done);
+    });
 
-  it('should issue separate sids', function(done){
-    var n = 0;
+    it('should pass session save error', function (done) {
+      var cb = after(2, done)
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, saveUninitialized: true }, function (req, res) {
+        res.end('session saved')
+      })
 
-    var app = express()
-      .use(cookieParser())
-      .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
-      .use(function(req, res){
-        req.session.count = ++n;
-        res.end();
+      store.set = function destroy(sid, sess, callback) {
+        callback(new Error('boom!'))
+      }
+
+      server.on('error', function onerror(err) {
+        err.message.should.equal('boom!')
+        cb()
       })
 
-    request(app)
-    .get('/')
-    .end(function(err, res){
+      request(server)
+      .get('/')
+      .expect(200, 'session saved', cb)
+    })
+  });
+
+  describe('unset option', function () {
+    it('should reject unknown values', function(){
+      session.bind(null, { unset: 'bogus!' }).should.throw(/unset.*must/);
+    });
+
+    it('should default to keep', function(done){
+      var store = new session.MemoryStore();
+      var app = express()
+        .use(session({ store: store, secret: 'keyboard cat' }))
+        .use(function(req, res, next){
+          req.session.count = req.session.count || 0;
+          req.session.count++;
+          if (req.session.count === 2) req.session = null;
+          res.end();
+        });
 
-      var id = sid(res);
       request(app)
       .get('/')
-      .set('Cookie', 'connect.sid=' + id)
-      .end(function(err, res){
-        sid(res).should.equal(id);
-
-        request(app)
-        .get('/')
-        .end(function(err, res){
-          sid(res).should.not.equal(id);
-          done();
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        store.length(function(err, len){
+          if (err) return done(err);
+          len.should.equal(1);
+          request(app)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, function(err, res){
+            if (err) return done(err);
+            store.length(function(err, len){
+              if (err) return done(err);
+              len.should.equal(1);
+              done();
+            });
+          });
         });
       });
     });
-  })
 
-  describe('req.session', function(){
-    it('should persist', function(done){
+    it('should allow destroy on req.session = null', function(done){
+      var store = new session.MemoryStore();
       var app = express()
-        .use(cookieParser())
-        .use(session({ secret: 'keyboard cat', cookie: { maxAge: min, httpOnly: false }}))
+        .use(session({ store: store, unset: 'destroy', secret: 'keyboard cat' }))
         .use(function(req, res, next){
-          // checks that cookie options persisted
-          req.session.cookie.httpOnly.should.equal(false);
-
           req.session.count = req.session.count || 0;
           req.session.count++;
-          res.end(req.session.count.toString());
+          if (req.session.count === 2) req.session = null;
+          res.end();
         });
 
       request(app)
       .get('/')
-      .end(function(err, res){
-        res.text.should.equal('1');
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        store.length(function(err, len){
+          if (err) return done(err);
+          len.should.equal(1);
+          request(app)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, function(err, res){
+            if (err) return done(err);
+            store.length(function(err, len){
+              if (err) return done(err);
+              len.should.equal(0);
+              done();
+            });
+          });
+        });
+      });
+    });
 
-        request(app)
-        .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
-        .end(function(err, res){
-          res.text.should.equal('2');
+    it('should not set cookie if initial session destroyed', function(done){
+      var store = new session.MemoryStore();
+      var app = express()
+        .use(session({ store: store, unset: 'destroy', secret: 'keyboard cat' }))
+        .use(function(req, res, next){
+          req.session = null;
+          res.end();
+        });
+
+      request(app)
+      .get('/')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        store.length(function(err, len){
+          if (err) return done(err);
+          len.should.equal(0);
+          should(cookie(res)).be.empty;
           done();
         });
       });
+    });
+
+    it('should pass session destroy error', function (done) {
+      var cb = after(2, done)
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store, unset: 'destroy' }, function (req, res) {
+        req.session = null
+        res.end('session destroyed')
+      })
+
+      store.destroy = function destroy(sid, callback) {
+        callback(new Error('boom!'))
+      }
+
+      server.on('error', function onerror(err) {
+        err.message.should.equal('boom!')
+        cb()
+      })
+
+      request(server)
+      .get('/')
+      .expect(200, 'session destroyed', cb)
+    })
+  });
+
+  describe('req.session', function(){
+    it('should persist', function(done){
+      var store = new session.MemoryStore()
+      var server = createServer({ store: store }, function (req, res) {
+        req.session.count = req.session.count || 0
+        req.session.count++
+        res.end('hits: ' + req.session.count)
+      })
+
+      request(server)
+      .get('/')
+      .expect(200, 'hits: 1', function (err, res) {
+        if (err) return done(err)
+        store.load(sid(res), function (err, sess) {
+          if (err) return done(err)
+          should(sess).not.be.empty
+          request(server)
+          .get('/')
+          .set('Cookie', cookie(res))
+          .expect(200, 'hits: 2', done)
+        })
+      })
     })
 
     it('should only set-cookie when modified', function(done){
       var modify = true;
 
       var app = express()
-        .use(cookieParser())
         .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
         .use(function(req, res, next){
           if (modify) {
@@ -412,15 +981,15 @@ describe('session()', function(){
 
         request(app)
         .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
+        .set('Cookie', cookie(res))
         .end(function(err, res){
-          var id = sid(res);
+          var val = cookie(res);
           res.text.should.equal('2');
           modify = false;
 
           request(app)
           .get('/')
-          .set('Cookie', 'connect.sid=' + sid(res))
+          .set('Cookie', val)
           .end(function(err, res){
             should(sid(res)).be.empty;
             res.text.should.equal('2');
@@ -428,7 +997,7 @@ describe('session()', function(){
 
             request(app)
             .get('/')
-            .set('Cookie', 'connect.sid=' + id)
+            .set('Cookie', val)
             .end(function(err, res){
               sid(res).should.not.be.empty;
               res.text.should.equal('3');
@@ -442,7 +1011,6 @@ describe('session()', function(){
     describe('.destroy()', function(){
       it('should destroy the previous session', function(done){
         var app = express()
-          .use(cookieParser())
           .use(session({ secret: 'keyboard cat' }))
           .use(function(req, res, next){
             req.session.destroy(function(err){
@@ -464,7 +1032,6 @@ describe('session()', function(){
     describe('.regenerate()', function(){
       it('should destroy/replace the previous session', function(done){
         var app = express()
-          .use(cookieParser())
           .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
           .use(function(req, res, next){
             var id = req.session.id;
@@ -478,25 +1045,96 @@ describe('session()', function(){
         request(app)
         .get('/')
         .end(function(err, res){
-          var id = sid(res);
-
+          if (err) return done(err)
+          var id = sid(res)
           request(app)
           .get('/')
-          .set('Cookie', 'connect.sid=' + id)
+          .set('Cookie', cookie(res))
           .end(function(err, res){
-            sid(res).should.not.equal('');
-            sid(res).should.not.equal(id);
+            if (err) return done(err)
+            should(sid(res)).not.be.empty
+            should(sid(res)).should.not.equal(id)
             done();
           });
         });
       })
     })
 
+    describe('.reload()', function () {
+      it('should reload session from store', function (done) {
+        var server = createServer(null, function (req, res) {
+          if (req.url === '/') {
+            req.session.active = true
+            res.end('session created')
+            return
+          }
+
+          req.session.url = req.url
+
+          if (req.url === '/bar') {
+            res.end('saw ' + req.session.url)
+            return
+          }
+
+          request(server)
+          .get('/bar')
+          .set('Cookie', val)
+          .expect(200, 'saw /bar', function (err, resp) {
+            if (err) return done(err)
+            req.session.reload(function (err) {
+              if (err) return done(err)
+              res.end('saw ' + req.session.url)
+            })
+          })
+        })
+        var val
+
+        request(server)
+        .get('/')
+        .expect(200, 'session created', function (err, res) {
+          if (err) return done(err)
+          val = cookie(res)
+          request(server)
+          .get('/foo')
+          .set('Cookie', val)
+          .expect(200, 'saw /bar', done)
+        })
+      })
+
+      it('should error is session missing', function (done) {
+        var store = new session.MemoryStore()
+        var server = createServer({ store: store }, function (req, res) {
+          if (req.url === '/') {
+            req.session.active = true
+            res.end('session created')
+            return
+          }
+
+          store.clear(function (err) {
+            if (err) return done(err)
+            req.session.reload(function (err) {
+              res.statusCode = err ? 500 : 200
+              res.end(err ? err.message : '')
+            })
+          })
+        })
+
+        request(server)
+        .get('/')
+        .expect(200, 'session created', function (err, res) {
+          if (err) return done(err)
+          request(server)
+          .get('/foo')
+          .set('Cookie', cookie(res))
+          .expect(500, 'failed to load session', done)
+        })
+      })
+    })
+
     describe('.cookie', function(){
       describe('.*', function(){
         it('should serialize as parameters', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', proxy: true, cookie: { maxAge: min }}))
             .use(function(req, res, next){
               req.session.cookie.httpOnly = false;
@@ -518,7 +1156,6 @@ describe('session()', function(){
 
         it('should default to a browser-session length cookie', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/admin' }}))
             .use(function(req, res, next){
               res.end();
@@ -536,7 +1173,6 @@ describe('session()', function(){
 
         it('should Set-Cookie only once for browser-session cookies', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/admin' }}))
             .use(function(req, res, next){
               res.end();
@@ -549,7 +1185,7 @@ describe('session()', function(){
 
             request(app)
             .get('/admin')
-            .set('Cookie', 'connect.sid=' + sid(res))
+            .set('Cookie', cookie(res))
             .end(function(err, res){
               res.headers.should.not.have.property('set-cookie');
               done();
@@ -559,7 +1195,6 @@ describe('session()', function(){
 
         it('should override defaults', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/admin', httpOnly: false, secure: true, maxAge: 5000 }}))
             .use(function(req, res, next){
               req.session.cookie.secure = false;
@@ -587,8 +1222,7 @@ describe('session()', function(){
           }
 
           var app = express()
-            .use(cookieParser('keyboard cat'))
-            .use(session())
+            .use(session({ secret: 'keyboard cat' }))
             .use(function(req, res, next){
               var cookie = new Cookie();
               res.setHeader('Set-Cookie', cookie.serialize('previous', 'cookieValue'));
@@ -607,7 +1241,6 @@ describe('session()', function(){
       describe('.secure', function(){
         it('should not set-cookie when insecure', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat' }))
             .use(function(req, res, next){
               req.session.cookie.secure = true;
@@ -626,7 +1259,6 @@ describe('session()', function(){
       describe('when the pathname does not match cookie.path', function(){
         it('should not set-cookie', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
             .use(function(req, res, next){
               if (!req.session) {
@@ -647,7 +1279,6 @@ describe('session()', function(){
 
         it('should not set-cookie even for FQDN', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
             .use(function(req, res, next){
               if (!req.session) {
@@ -672,7 +1303,6 @@ describe('session()', function(){
       describe('when the pathname does match cookie.path', function(){
         it('should set-cookie', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
             .use(function(req, res, next){
               req.session.foo = Math.random();
@@ -689,7 +1319,6 @@ describe('session()', function(){
 
         it('should set-cookie even for FQDN', function(done){
           var app = express()
-            .use(cookieParser())
             .use(session({ secret: 'keyboard cat', cookie: { path: '/foo/bar' }}))
             .use(function(req, res, next){
               req.session.foo = Math.random();
@@ -708,9 +1337,8 @@ describe('session()', function(){
       })
 
       describe('.maxAge', function(){
-        var id;
+        var val;
         var app = express()
-          .use(cookieParser())
           .use(session({ secret: 'keyboard cat', cookie: { maxAge: 2000 }}))
           .use(function(req, res, next){
             req.session.count = req.session.count || 0;
@@ -727,7 +1355,7 @@ describe('session()', function(){
             var a = new Date(expires(res))
               , b = new Date;
 
-            id = sid(res);
+            val = cookie(res);
 
             a.getYear().should.equal(b.getYear());
             a.getMonth().should.equal(b.getMonth());
@@ -743,12 +1371,12 @@ describe('session()', function(){
         it('should modify cookie when changed', function(done){
           request(app)
           .get('/')
-          .set('Cookie', 'connect.sid=' + id)
+          .set('Cookie', val)
           .end(function(err, res){
             var a = new Date(expires(res))
               , b = new Date;
 
-            id = sid(res);
+            val = cookie(res);
 
             a.getYear().should.equal(b.getYear());
             a.getMonth().should.equal(b.getMonth());
@@ -763,12 +1391,12 @@ describe('session()', function(){
         it('should modify cookie when changed to large value', function(done){
           request(app)
           .get('/')
-          .set('Cookie', 'connect.sid=' + id)
+          .set('Cookie', val)
           .end(function(err, res){
             var a = new Date(expires(res))
               , b = new Date;
 
-            id = sid(res);
+            val = cookie(res);
 
             var delta = a.valueOf() - b.valueOf();
             (delta > 2999999000 && delta < 3000000000).should.be.ok;
@@ -782,7 +1410,6 @@ describe('session()', function(){
         describe('when given a Date', function(){
           it('should set absolute', function(done){
             var app = express()
-              .use(cookieParser())
               .use(session({ secret: 'keyboard cat' }))
               .use(function(req, res, next){
                 req.session.cookie.expires = new Date(0);
@@ -801,7 +1428,6 @@ describe('session()', function(){
         describe('when null', function(){
           it('should be a browser-session cookie', function(done){
             var app = express()
-              .use(cookieParser())
               .use(session({ secret: 'keyboard cat' }))
               .use(function(req, res, next){
                 req.session.cookie.expires = null;
@@ -820,30 +1446,134 @@ describe('session()', function(){
         })
       })
     })
+  })
+
+  describe('synchronous store', function(){
+    it('should respond correctly on save', function(done){
+      var store = new SyncStore()
+      var server = createServer({ store: store }, function (req, res) {
+        req.session.count = req.session.count || 0
+        req.session.count++
+        res.end('hits: ' + req.session.count)
+      })
+
+      request(server)
+      .get('/')
+      .expect(200, 'hits: 1', done)
+    })
 
-    it('should support req.signedCookies', function(done){
+    it('should respond correctly on destroy', function(done){
+      var store = new SyncStore()
+      var server = createServer({ store: store, unset: 'destroy' }, function (req, res) {
+        req.session.count = req.session.count || 0
+        var count = ++req.session.count
+        if (req.session.count > 1) {
+          req.session = null
+          res.write('destroyed\n')
+        }
+        res.end('hits: ' + count)
+      })
+
+      request(server)
+      .get('/')
+      .expect(200, 'hits: 1', function (err, res) {
+        if (err) return done(err)
+        request(server)
+        .get('/')
+        .set('Cookie', cookie(res))
+        .expect(200, 'destroyed\nhits: 2', done)
+      })
+    })
+  })
+
+  describe('cookieParser()', function () {
+    it('should read from req.cookies', function(done){
+      var app = express()
+        .use(cookieParser())
+        .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() })
+        .use(session({ secret: 'keyboard cat' }))
+        .use(function(req, res, next){
+          req.session.count = req.session.count || 0
+          req.session.count++
+          res.end(req.session.count.toString())
+        })
+
+      request(app)
+      .get('/')
+      .expect(200, '1', function (err, res) {
+        if (err) return done(err)
+        request(app)
+        .get('/')
+        .set('Cookie', cookie(res))
+        .expect(200, '2', done)
+      })
+    })
+
+    it('should reject unsigned from req.cookies', function(done){
+      var app = express()
+        .use(cookieParser())
+        .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() })
+        .use(session({ secret: 'keyboard cat', key: 'sessid' }))
+        .use(function(req, res, next){
+          req.session.count = req.session.count || 0
+          req.session.count++
+          res.end(req.session.count.toString())
+        })
+
+      request(app)
+      .get('/')
+      .expect(200, '1', function (err, res) {
+        if (err) return done(err)
+        request(app)
+        .get('/')
+        .set('Cookie', 'sessid=' + sid(res))
+        .expect(200, '1', done)
+      })
+    })
+
+    it('should reject invalid signature from req.cookies', function(done){
+      var app = express()
+        .use(cookieParser())
+        .use(function(req, res, next){ req.headers.cookie = 'foo=bar'; next() })
+        .use(session({ secret: 'keyboard cat', key: 'sessid' }))
+        .use(function(req, res, next){
+          req.session.count = req.session.count || 0
+          req.session.count++
+          res.end(req.session.count.toString())
+        })
+
+      request(app)
+      .get('/')
+      .expect(200, '1', function (err, res) {
+        if (err) return done(err)
+        var val = cookie(res).replace(/...\./, '.')
+        request(app)
+        .get('/')
+        .set('Cookie', val)
+        .expect(200, '1', done)
+      })
+    })
+
+    it('should read from req.signedCookies', function(done){
       var app = express()
         .use(cookieParser('keyboard cat'))
+        .use(function(req, res, next){ delete req.headers.cookie; next() })
         .use(session())
         .use(function(req, res, next){
-          req.session.count = req.session.count || 0;
-          req.session.count++;
-          res.end(req.session.count.toString());
-        });
+          req.session.count = req.session.count || 0
+          req.session.count++
+          res.end(req.session.count.toString())
+        })
 
       request(app)
       .get('/')
-      .end(function(err, res){
-        res.text.should.equal('1');
-
+      .expect(200, '1', function (err, res) {
+        if (err) return done(err)
         request(app)
         .get('/')
-        .set('Cookie', 'connect.sid=' + sid(res))
-        .end(function(err, res){
-          res.text.should.equal('2');
-          done();
-        });
-      });
+        .set('Cookie', cookie(res))
+        .expect(200, '2', done)
+      })
     })
   })
 })
@@ -853,12 +1583,71 @@ function cookie(res) {
   return (setCookie && setCookie[0]) || undefined;
 }
 
+function createServer(opts, fn) {
+  var options = opts || {}
+  var respond = fn || end
+
+  if (!('cookie' in options)) {
+    options.cookie = { maxAge: 60 * 1000 }
+  }
+
+  if (!('secret' in options)) {
+    options.secret = 'keyboard cat'
+  }
+
+  var _session = session(options)
+
+  var server = http.createServer(function (req, res) {
+    _session(req, res, function (err) {
+      if (err && !res._header) {
+        res.statusCode = err.status || 500
+        res.end(err.message)
+        return
+      }
+
+      if (err) {
+        server.emit('error', err)
+        return
+      }
+
+      respond(req, res)
+    })
+  })
+
+  return server
+}
+
+function end(req, res) {
+  res.end()
+}
+
 function expires(res) {
   var match = /Expires=([^;]+)/.exec(cookie(res));
   return match ? match[1] : undefined;
 }
 
 function sid(res) {
-  var match = /^connect\.sid=([^;]+);/.exec(cookie(res));
-  return match ? match[1] : undefined;
+  var match = /^[^=]+=s%3A([^;\.]+)[\.;]/.exec(cookie(res))
+  var val = match ? match[1] : undefined
+  return val
+}
+
+function SyncStore() {
+  this.sessions = Object.create(null);
 }
+
+SyncStore.prototype.__proto__ = session.Store.prototype;
+
+SyncStore.prototype.destroy = function destroy(sid, callback) {
+  delete this.sessions[sid];
+  callback();
+};
+
+SyncStore.prototype.get = function get(sid, callback) {
+  callback(null, JSON.parse(this.sessions[sid]));
+};
+
+SyncStore.prototype.set = function set(sid, sess, callback) {
+  this.sessions[sid] = JSON.stringify(sess);
+  callback();
+};

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-javascript/node-express-session.git



More information about the Pkg-javascript-commits mailing list