[Pkg-javascript-commits] [node-express-session] 01/01: Imported Upstream version 1.3.1

Leo Iannacone l3on-guest at moszumanska.debian.org
Tue Jun 17 13:47:50 UTC 2014


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

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

commit 897220501199ac5aebec8dc1e32d0e8b9881d6a8
Author: Leo Iannacone <l3on at ubuntu.com>
Date:   Tue Jun 17 14:35:00 2014 +0200

    Imported Upstream version 1.3.1
---
 .npmignore         |   3 +
 .travis.yml        |  10 +
 History.md         |  52 ++++
 LICENSE            |  22 ++
 README.md          | 190 ++++++++++++
 index.js           | 291 ++++++++++++++++++
 package.json       |  33 ++
 session/cookie.js  | 128 ++++++++
 session/memory.js  | 137 +++++++++
 session/session.js | 116 +++++++
 session/store.js   |  84 ++++++
 test/session.js    | 864 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 12 files changed, 1930 insertions(+)

diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..cd39b77
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,3 @@
+coverage/
+test/
+.travis.yml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..bb47c1b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: node_js
+node_js:
+  - "0.8"
+  - "0.10"
+  - "0.11"
+matrix:
+  allow_failures:
+    - node_js: "0.11"
+  fast_finish: true
+script: "npm run-script test-travis"
diff --git a/History.md b/History.md
new file mode 100644
index 0000000..df529d2
--- /dev/null
+++ b/History.md
@@ -0,0 +1,52 @@
+1.3.1 / 2014-06-14
+==================
+
+  * Add description in package for npmjs.org listing
+
+1.3.0 / 2014-06-14
+==================
+
+  * Integrate with express "trust proxy" by default
+  * deps: debug at 1.0.2
+
+1.2.1 / 2014-05-27
+==================
+
+  * Fix `resave` such that `resave: true` works
+
+1.2.0 / 2014-05-19
+==================
+
+  * Add `resave` option to control saving unmodified sessions
+
+1.1.0 / 2014-05-12
+==================
+
+  * Add `name` option; replacement for `key` option
+  * Use `setImmediate` in MemoryStore for node.js >= 0.10
+
+1.0.4 / 2014-04-27
+==================
+
+  * deps: debug at 0.8.1
+
+1.0.3 / 2014-04-19
+==================
+
+  *  Use `res.cookie()` instead of `res.setHeader()`
+  * deps: cookie at 0.1.2
+
+1.0.2 / 2014-02-23
+==================
+
+  * Add missing dependency to `package.json`
+
+1.0.1 / 2014-02-15
+==================
+
+  * Add missing dependencies to `package.json`
+
+1.0.0 / 2014-02-15
+==================
+
+  * Genesis from `connect`
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a7693b0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2014 TJ Holowaychuk <tj at vision-media.ca>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2905768
--- /dev/null
+++ b/README.md
@@ -0,0 +1,190 @@
+# express-session
+
+[![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)
+
+THIS REPOSITORY NEEDS A MAINTAINER. IF YOU'RE INTERESTED IN MAINTAINING THIS REPOSITORY, PLEASE LET US KNOW!
+
+## API
+
+```js
+var express      = require('express')
+var cookieParser = require('cookie-parser')
+var session      = require('express-session')
+
+var app = express()
+
+app.use(cookieParser()) // required before session.
+app.use(session({secret: 'keyboard cat'}))
+```
+
+
+### session(options)
+
+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()`.
+
+#### Options
+
+  - `name` - cookie name (formerly known as `key`). (default: `'connect.sid'`)
+  - `store` - session store instance.
+  - `secret` - session cookie is signed with this secret to prevent tampering.
+  - `cookie` - session cookie settings.
+    - (default: `{ path: '/', httpOnly: true, secure: false, maxAge: null }`)
+  - `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`)
+
+
+#### Cookie options
+
+Please note that `secure: true` is a **recommended** option. However, it requires an https-enabled website, i.e., HTTPS is necessary for secure cookies.
+If `secure` is set, and you access your site over HTTP, the cookie will not be set. If you have your node.js behind a proxy and are using `secure: true`, you need to set "trust proxy" in express:
+
+```js
+var app = express()
+app.set('trust proxy', 1) // trust first proxy
+app.use(cookieParser())
+app.use(session({
+    secret: 'keyboard cat'
+  , cookie: { secure: true }
+}))
+```
+
+For using secure cookies in production, but allowing for testing in development, the following is an example of enabling this setup based on `NODE_ENV` in express:
+
+```js
+var app = express()
+var sess = {
+  secret: 'keyboard cat'
+  cookie: {}
+}
+
+if (app.get('env') === 'production') {
+  app.set('trust proxy', 1) // trust first proxy
+  sess.cookie.secure = true // serve secure cookies
+}
+
+app.use(cookieParser())
+app.use(session(sess))
+```
+
+By default `cookie.maxAge` is `null`, meaning no "expires" parameter is set
+so the cookie becomes a browser-session cookie. When the user closes the
+browser the cookie (and session) will be removed.
+
+### req.session
+
+To store or access session data, simply use the request property `req.session`,
+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) {
+  var sess = req.session
+  if (sess.views) {
+    sess.views++
+    res.setHeader('Content-Type', 'text/html')
+    res.write('<p>views: ' + sess.views + '</p>')
+    res.write('<p>expires in: ' + (sess.cookie.maxAge / 1000) + 's</p>')
+    res.end()
+  } else {
+    sess.views = 1
+    res.end('welcome to the session demo. refresh!')
+  }
+})
+```
+
+#### Session.regenerate()
+
+To regenerate the session simply invoke the method, once complete
+a new SID and `Session` instance will be initialized at `req.session`.
+
+```js
+req.session.regenerate(function(err) {
+  // will have a new session here
+})
+```
+
+#### Session.destroy()
+
+Destroys the session, removing `req.session`, will be re-generated next request.
+
+```js
+req.session.destroy(function(err) {
+  // cannot access session here
+})
+```
+
+#### Session.reload()
+
+Reloads the session data.
+
+```js
+req.session.reload(function(err) {
+  // session updated
+})
+```
+
+#### Session.save()
+
+```js
+req.session.save(function(err) {
+  // session saved
+})
+```
+
+#### Session.touch()
+
+Updates the `.maxAge` property. Typically this is
+not necessary to call, as the session middleware does this for you.
+
+### req.session.cookie
+
+Each session has a unique cookie object accompany it. This allows
+you to alter the session cookie per visitor. For example we can
+set `req.session.cookie.expires` to `false` to enable the cookie
+to remain for only the duration of the user-agent.
+
+#### Cookie.maxAge
+
+Alternatively `req.session.cookie.maxAge` will return the time
+remaining in milliseconds, which we may also re-assign a new value
+to adjust the `.expires` property appropriately. The following
+are essentially equivalent
+
+```js
+var hour = 3600000
+req.session.cookie.expires = new Date(Date.now() + hour)
+req.session.cookie.maxAge = hour
+```
+
+For example when `maxAge` is set to `60000` (one minute), and 30 seconds
+has elapsed it will return `30000` until the current request has completed,
+at which time `req.session.touch()` is called to reset `req.session.maxAge`
+to its original value.
+
+```js
+req.session.cookie.maxAge // => 30000
+```
+
+## Session Store Implementation
+
+Every session store _must_ implement the following methods
+
+   - `.get(sid, callback)`
+   - `.set(sid, session, callback)`
+   - `.destroy(sid, callback)`
+
+Recommended methods include, but are not limited to:
+
+   - `.length(callback)`
+   - `.clear(callback)`
+
+For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo.
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..d48bcd1
--- /dev/null
+++ b/index.js
@@ -0,0 +1,291 @@
+/*!
+ * express-session
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var uid = require('uid2')
+  , 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')
+  , Cookie = require('./session/cookie')
+  , 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.
+ */
+
+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.';
+
+/**
+ * Setup session store with the given `options`.
+ *
+ * See README.md for documentation of options and formatting.
+ *
+ * Session data is _not_ saved in the cookie itself, however cookies are used,
+ * so you must use the cookie-parser middleware _before_ `session()`.
+ * [https://github.com/expressjs/cookie-parser]
+ *
+ * @param {Object} options
+ * @return {Function} middleware
+ * @api public
+ */
+
+function session(options){
+  var options = options || {}
+  //  name - previously "options.key"
+    , name = options.name || options.key || 'connect.sid'
+    , store = options.store || new MemoryStore
+    , cookie = options.cookie || {}
+    , trustProxy = options.proxy
+    , storeReady = true
+    , rollingSessions = options.rolling || false;
+
+  // TODO: switch default to false on next major
+  var resaveSession = options.resave === undefined
+    ? true
+    : options.resave;
+
+  // notify user that this store is not
+  // meant for a production environment
+  if ('production' == env && store instanceof MemoryStore) {
+    console.warn(warning);
+  }
+
+  // generates the new session
+  store.generate = function(req){
+    req.sessionID = uid(24);
+    req.session = new Session(req);
+    req.session.cookie = new Cookie(cookie);
+  };
+
+  store.on('disconnect', function(){ storeReady = false; });
+  store.on('connect', function(){ storeReady = true; });
+
+  return function session(req, res, next) {
+    // self-awareness
+    if (req.session) return next();
+
+    // Handle connection as if there is no session if
+    // the store has temporarily disconnected etc
+    if (!storeReady) return debug('store is disconnected'), next();
+
+    // pathname mismatch
+    var originalPath = parse(req.originalUrl).pathname;
+    if (0 != originalPath.indexOf(cookie.path || '/')) return next();
+
+    // backwards compatibility for signed cookies
+    // req.secret is passed from the cookie parser middleware
+    var secret = options.secret || req.secret;
+
+    // ensure secret is available or bail
+    if (!secret) throw new Error('`secret` option required for sessions');
+
+    var originalHash
+      , originalId;
+
+    // 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;
+    }
+
+    // set-cookie
+    onHeaders(res, function(){
+      if (!req.session) {
+        debug('no session');
+        return;
+      }
+
+      var cookie = req.session.cookie;
+
+      // only send secure cookies via https
+      if (cookie.secure && !issecure(req, trustProxy)) {
+        debug('not secured');
+        return;
+      }
+
+      var isNew = unsignedCookie != req.sessionID;
+
+      // in case of rolling session, always reset the cookie
+      if (!rollingSessions) {
+
+        // browser-session length cookie
+        if (null == cookie.expires) {
+          if (!isNew) {
+            debug('already set browser-session cookie');
+            return
+          }
+        // compare hashes and ids
+        } else if (!isModified(req.session)) {
+          debug('unmodified session');
+          return
+        }
+
+      }
+
+      var val = 's:' + signature.sign(req.sessionID, secret);
+      debug('set-cookie %s', val);
+      res.cookie(name, val, cookie.data);
+    });
+
+    // 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)) {
+        debug('saving');
+        return req.session.save(function(err){
+          if (err) console.error(err.stack);
+          debug('saved');
+          res.end(data, encoding);
+        });
+      }
+
+      res.end(data, encoding);
+    };
+
+    // generate the session
+    function generate() {
+      store.generate(req);
+    }
+
+    // check if session has been modified
+    function isModified(sess) {
+      return originalHash != hash(sess) || originalId != sess.id;
+    }
+
+    // get the sessionID from the cookie
+    req.sessionID = unsignedCookie;
+
+    // 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) {
+        debug('error %j', err);
+        if ('ENOENT' == err.code) {
+          generate();
+          next();
+        } else {
+          next(err);
+        }
+      // no session
+      } else if (!sess) {
+        debug('no session found');
+        generate();
+        next();
+      // populate req.session
+      } else {
+        debug('session found');
+        store.createSession(req, sess);
+        originalId = req.sessionID;
+        originalHash = hash(sess);
+        next();
+      }
+    });
+  };
+};
+
+/**
+ * Hash the given `sess` object omitting changes to `.cookie`.
+ *
+ * @param {Object} sess
+ * @return {String}
+ * @api private
+ */
+
+function hash(sess) {
+  return crc32.signed(JSON.stringify(sess, function(key, val){
+    if ('cookie' != key) return val;
+  }));
+}
+
+/**
+ * Determine if request is secure.
+ *
+ * @param {Object} req
+ * @param {Boolean} [trustProxy]
+ * @return {Boolean}
+ * @api 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) {
+    var secure = req.secure;
+    return typeof secure === 'boolean'
+      ? secure
+      : false;
+  }
+
+  // read the proto from x-forwarded-proto header
+  var header = req.headers['x-forwarded-proto'] || '';
+  var index = header.indexOf(',');
+  var proto = index !== -1
+    ? header.substr(0, index).toLowerCase().trim()
+    : header.toLowerCase().trim()
+
+  return proto === 'https';
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..df44c60
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+  "name": "express-session",
+  "version": "1.3.1",
+  "description": "Simple session middleware for Express",
+  "author": "TJ Holowaychuk <tj at vision-media.ca> (http://tjholowaychuk.com)",
+  "repository": "expressjs/session",
+  "license": "MIT",
+  "dependencies": {
+    "buffer-crc32": "0.2.1",
+    "cookie": "0.1.2",
+    "cookie-signature": "1.0.3",
+    "debug": "1.0.2",
+    "on-headers": "0.0.0",
+    "uid2": "0.0.3",
+    "utils-merge": "1.0.0"
+  },
+  "devDependencies": {
+    "cookie-parser": "1.1.0",
+    "istanbul": "0.2.10",
+    "express": "~4.4.0",
+    "mocha": "~1.20.1",
+    "should": "~4.0.4",
+    "supertest": "~0.13.0"
+  },
+  "engines": {
+    "node": ">= 0.8.0"
+  },
+  "scripts": {
+    "test": "mocha --bail --reporter spec test/",
+    "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot test/",
+    "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec test/"
+  }
+}
diff --git a/session/cookie.js b/session/cookie.js
new file mode 100644
index 0000000..86591de
--- /dev/null
+++ b/session/cookie.js
@@ -0,0 +1,128 @@
+
+/*!
+ * Connect - session - Cookie
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var merge = require('utils-merge')
+  , cookie = require('cookie');
+
+/**
+ * Initialize a new `Cookie` with the given `options`.
+ *
+ * @param {IncomingMessage} req
+ * @param {Object} options
+ * @api private
+ */
+
+var Cookie = module.exports = function Cookie(options) {
+  this.path = '/';
+  this.maxAge = null;
+  this.httpOnly = true;
+  if (options) merge(this, options);
+  this.originalMaxAge = undefined == this.originalMaxAge
+    ? this.maxAge
+    : this.originalMaxAge;
+};
+
+/*!
+ * Prototype.
+ */
+
+Cookie.prototype = {
+
+  /**
+   * Set expires `date`.
+   *
+   * @param {Date} date
+   * @api public
+   */
+
+  set expires(date) {
+    this._expires = date;
+    this.originalMaxAge = this.maxAge;
+  },
+
+  /**
+   * Get expires `date`.
+   *
+   * @return {Date}
+   * @api public
+   */
+
+  get expires() {
+    return this._expires;
+  },
+
+  /**
+   * Set expires via max-age in `ms`.
+   *
+   * @param {Number} ms
+   * @api public
+   */
+
+  set maxAge(ms) {
+    this.expires = 'number' == typeof ms
+      ? new Date(Date.now() + ms)
+      : ms;
+  },
+
+  /**
+   * Get expires max-age in `ms`.
+   *
+   * @return {Number}
+   * @api public
+   */
+
+  get maxAge() {
+    return this.expires instanceof Date
+      ? this.expires.valueOf() - Date.now()
+      : this.expires;
+  },
+
+  /**
+   * Return cookie data object.
+   *
+   * @return {Object}
+   * @api private
+   */
+
+  get data() {
+    return {
+        originalMaxAge: this.originalMaxAge
+      , expires: this._expires
+      , secure: this.secure
+      , httpOnly: this.httpOnly
+      , domain: this.domain
+      , path: this.path
+    }
+  },
+
+  /**
+   * Return a serialized cookie string.
+   *
+   * @return {String}
+   * @api public
+   */
+
+  serialize: function(name, val){
+    return cookie.serialize(name, val, this.data);
+  },
+
+  /**
+   * Return JSON representation of this cookie.
+   *
+   * @return {Object}
+   * @api private
+   */
+
+  toJSON: function(){
+    return this.data;
+  }
+};
diff --git a/session/memory.js b/session/memory.js
new file mode 100644
index 0000000..9720b06
--- /dev/null
+++ b/session/memory.js
@@ -0,0 +1,137 @@
+
+/*!
+ * Connect - session - MemoryStore
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var Store = require('./store');
+
+/**
+ * Shim setImmediate for node.js < 0.10
+ */
+
+var asyncTick = typeof setImmediate === 'function'
+  ? setImmediate
+  : process.nextTick;
+
+/**
+ * Initialize a new `MemoryStore`.
+ *
+ * @api public
+ */
+
+var MemoryStore = module.exports = function MemoryStore() {
+  this.sessions = {};
+};
+
+/**
+ * Inherit from `Store.prototype`.
+ */
+
+MemoryStore.prototype.__proto__ = Store.prototype;
+
+/**
+ * Attempt to fetch session by the given `sid`.
+ *
+ * @param {String} sid
+ * @param {Function} fn
+ * @api public
+ */
+
+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();
+    }
+  });
+};
+
+/**
+ * Commit the given `sess` object associated with the given `sid`.
+ *
+ * @param {String} sid
+ * @param {Session} sess
+ * @param {Function} fn
+ * @api public
+ */
+
+MemoryStore.prototype.set = function(sid, sess, fn){
+  var self = this;
+  asyncTick(function(){
+    self.sessions[sid] = JSON.stringify(sess);
+    fn && fn();
+  });
+};
+
+/**
+ * Destroy the session associated with the given `sid`.
+ *
+ * @param {String} sid
+ * @api public
+ */
+
+MemoryStore.prototype.destroy = function(sid, fn){
+  var self = this;
+  asyncTick(function(){
+    delete self.sessions[sid];
+    fn && fn();
+  });
+};
+
+/**
+ * Invoke the given callback `fn` with all active sessions.
+ *
+ * @param {Function} fn
+ * @api public
+ */
+
+MemoryStore.prototype.all = function(fn){
+  var arr = []
+    , keys = Object.keys(this.sessions);
+  for (var i = 0, len = keys.length; i < len; ++i) {
+    arr.push(this.sessions[keys[i]]);
+  }
+  fn(null, arr);
+};
+
+/**
+ * Clear all sessions.
+ *
+ * @param {Function} fn
+ * @api public
+ */
+
+MemoryStore.prototype.clear = function(fn){
+  this.sessions = {};
+  fn && fn();
+};
+
+/**
+ * Fetch number of sessions.
+ *
+ * @param {Function} fn
+ * @api public
+ */
+
+MemoryStore.prototype.length = function(fn){
+  fn(null, Object.keys(this.sessions).length);
+};
diff --git a/session/session.js b/session/session.js
new file mode 100644
index 0000000..891f31c
--- /dev/null
+++ b/session/session.js
@@ -0,0 +1,116 @@
+
+/*!
+ * Connect - session - Session
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var merge = require('utils-merge')
+
+/**
+ * Create a new `Session` with the given request and `data`.
+ *
+ * @param {IncomingRequest} req
+ * @param {Object} data
+ * @api private
+ */
+
+var Session = module.exports = function Session(req, data) {
+  Object.defineProperty(this, 'req', { value: req });
+  Object.defineProperty(this, 'id', { value: req.sessionID });
+  if ('object' == typeof data) merge(this, data);
+};
+
+/**
+ * Update reset `.cookie.maxAge` to prevent
+ * the cookie from expiring when the
+ * session is still active.
+ *
+ * @return {Session} for chaining
+ * @api public
+ */
+
+Session.prototype.touch = function(){
+  return this.resetMaxAge();
+};
+
+/**
+ * Reset `.maxAge` to `.originalMaxAge`.
+ *
+ * @return {Session} for chaining
+ * @api public
+ */
+
+Session.prototype.resetMaxAge = function(){
+  this.cookie.maxAge = this.cookie.originalMaxAge;
+  return this;
+};
+
+/**
+ * Save the session data with optional callback `fn(err)`.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+Session.prototype.save = function(fn){
+  this.req.sessionStore.set(this.id, this, fn || function(){});
+  return this;
+};
+
+/**
+ * Re-loads the session data _without_ altering
+ * the maxAge properties. Invokes the callback `fn(err)`,
+ * after which time if no exception has occurred the
+ * `req.session` property will be a new `Session` object,
+ * although representing the same session.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+Session.prototype.reload = function(fn){
+  var req = this.req
+    , store = this.req.sessionStore;
+  store.get(this.id, function(err, sess){
+    if (err) return fn(err);
+    if (!sess) return fn(new Error('failed to load session'));
+    store.createSession(req, sess);
+    fn();
+  });
+  return this;
+};
+
+/**
+ * Destroy `this` session.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+Session.prototype.destroy = function(fn){
+  delete this.req.session;
+  this.req.sessionStore.destroy(this.id, fn);
+  return this;
+};
+
+/**
+ * Regenerate this request's session.
+ *
+ * @param {Function} fn
+ * @return {Session} for chaining
+ * @api public
+ */
+
+Session.prototype.regenerate = function(fn){
+  this.req.sessionStore.regenerate(this.req, fn);
+  return this;
+};
diff --git a/session/store.js b/session/store.js
new file mode 100644
index 0000000..54294cb
--- /dev/null
+++ b/session/store.js
@@ -0,0 +1,84 @@
+
+/*!
+ * Connect - session - Store
+ * Copyright(c) 2010 Sencha Inc.
+ * Copyright(c) 2011 TJ Holowaychuk
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('events').EventEmitter
+  , Session = require('./session')
+  , Cookie = require('./cookie');
+
+/**
+ * Initialize abstract `Store`.
+ *
+ * @api private
+ */
+
+var Store = module.exports = function Store(options){};
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+Store.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Re-generate the given requests's session.
+ *
+ * @param {IncomingRequest} req
+ * @return {Function} fn
+ * @api public
+ */
+
+Store.prototype.regenerate = function(req, fn){
+  var self = this;
+  this.destroy(req.sessionID, function(err){
+    self.generate(req);
+    fn(err);
+  });
+};
+
+/**
+ * Load a `Session` instance via the given `sid`
+ * and invoke the callback `fn(err, sess)`.
+ *
+ * @param {String} sid
+ * @param {Function} fn
+ * @api public
+ */
+
+Store.prototype.load = function(sid, fn){
+  var self = this;
+  this.get(sid, function(err, sess){
+    if (err) return fn(err);
+    if (!sess) return fn();
+    var req = { sessionID: sid, sessionStore: self };
+    sess = self.createSession(req, sess);
+    fn(null, sess);
+  });
+};
+
+/**
+ * Create session from JSON `sess` data.
+ *
+ * @param {IncomingRequest} req
+ * @param {Object} sess
+ * @return {Session}
+ * @api private
+ */
+
+Store.prototype.createSession = function(req, sess){
+  var expires = sess.cookie.expires
+    , orig = sess.cookie.originalMaxAge;
+  sess.cookie = new Cookie(sess.cookie);
+  if ('string' == typeof expires) sess.cookie.expires = new Date(expires);
+  sess.cookie.originalMaxAge = orig;
+  req.session = new Session(req, sess);
+  return req.session;
+};
diff --git a/test/session.js b/test/session.js
new file mode 100644
index 0000000..54622bf
--- /dev/null
+++ b/test/session.js
@@ -0,0 +1,864 @@
+
+var express = require('express')
+  , assert = require('assert')
+  , request = require('supertest')
+  , should = require('should')
+  , cookieParser = require('cookie-parser')
+  , session = require('../')
+  , Cookie = require('../session/cookie')
+
+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;
+    session.Store.should.be.a.Function;
+    session.MemoryStore.should.be.a.Function;
+  })
+
+  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);
+
+        request(app)
+        .get('/')
+        .set('X-Forwarded-Proto', 'https')
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).not.be.empty;
+          done();
+        });
+      })
+
+      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)
+        .get('/')
+        .set('X-Forwarded-Proto', 'https,http')
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).not.be.empty;
+          done();
+        });
+      })
+
+      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)
+        .get('/')
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).be.empty;
+          done();
+        });
+      })
+    })
+
+    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);
+
+        request(app)
+        .get('/')
+        .set('X-Forwarded-Proto', 'https')
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).be.empty;
+          done();
+        });
+      })
+
+      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');
+
+        request(app)
+        .get('/')
+        .set('X-Forwarded-Proto', 'https')
+        .expect(200, 'true', function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).be.empty;
+          done();
+        });
+      })
+    })
+
+    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);
+
+        request(app)
+        .get('/')
+        .set('X-Forwarded-Proto', 'https')
+        .expect(200, function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).be.empty;
+          done();
+        });
+      })
+
+      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');
+
+        request(app)
+        .get('/')
+        .set('X-Forwarded-Proto', 'https')
+        .expect(200, 'true', function(err, res){
+          if (err) return done(err);
+          should(cookie(res)).not.be.empty;
+          done();
+        });
+      })
+    })
+  })
+
+  describe('key option', function(){
+    it('should default to "connect.sid"', function(done){
+      request(app)
+      .get('/')
+      .end(function(err, res){
+        res.headers['set-cookie'].should.have.length(1);
+        res.headers['set-cookie'][0].should.match(/^connect\.sid/);
+        done();
+      });
+    })
+
+    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)
+      .get('/')
+      .end(function(err, res){
+        res.headers['set-cookie'].should.have.length(1);
+        res.headers['set-cookie'][0].should.match(/^sid/);
+        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;
+        res.setHeader('x-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('/')
+      .expect('x-count', '1')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .expect('x-count', '2')
+        .expect(200, done);
+      });
+    });
+
+    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;
+        res.setHeader('x-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('/')
+      .expect('x-count', '1')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .expect('x-count', '2')
+        .expect(200, done);
+      });
+    });
+
+    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;
+        res.setHeader('x-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('/')
+      .expect('x-count', '1')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .expect('x-count', '1')
+        .expect(200, done);
+      });
+    });
+
+    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;
+        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('/')
+      .expect('x-count', '1')
+      .expect(200, function(err, res){
+        if (err) return done(err);
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .expect('x-count', '2')
+        .expect(200, done);
+      });
+    });
+  });
+
+  it('should retain the sid', function(done){
+    var n = 0;
+
+    var app = express()
+      .use(cookieParser())
+      .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
+      .use(function(req, res){
+        req.session.count = ++n;
+        res.end();
+      })
+
+    request(app)
+    .get('/')
+    .end(function(err, res){
+
+      var id = sid(res);
+      request(app)
+      .get('/')
+      .set('Cookie', 'connect.sid=' + id)
+      .end(function(err, res){
+        sid(res).should.equal(id);
+        done();
+      });
+    });
+  })
+
+  describe('when an invalid sid is given', function(){
+    it('should generate a new one', function(done){
+      request(app)
+      .get('/')
+      .set('Cookie', 'connect.sid=foobarbaz')
+      .end(function(err, res){
+        sid(res).should.not.equal('foobarbaz');
+        done();
+      });
+    })
+  })
+
+  it('should issue separate sids', function(done){
+    var n = 0;
+
+    var app = express()
+      .use(cookieParser())
+      .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }}))
+      .use(function(req, res){
+        req.session.count = ++n;
+        res.end();
+      })
+
+    request(app)
+    .get('/')
+    .end(function(err, res){
+
+      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();
+        });
+      });
+    });
+  })
+
+  describe('req.session', function(){
+    it('should persist', function(done){
+      var app = express()
+        .use(cookieParser())
+        .use(session({ secret: 'keyboard cat', cookie: { maxAge: min, httpOnly: false }}))
+        .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());
+        });
+
+      request(app)
+      .get('/')
+      .end(function(err, res){
+        res.text.should.equal('1');
+
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .end(function(err, res){
+          res.text.should.equal('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) {
+            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');
+
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .end(function(err, res){
+          var id = sid(res);
+          res.text.should.equal('2');
+          modify = false;
+
+          request(app)
+          .get('/')
+          .set('Cookie', 'connect.sid=' + sid(res))
+          .end(function(err, res){
+            should(sid(res)).be.empty;
+            res.text.should.equal('2');
+            modify = true;
+
+            request(app)
+            .get('/')
+            .set('Cookie', 'connect.sid=' + id)
+            .end(function(err, res){
+              sid(res).should.not.be.empty;
+              res.text.should.equal('3');
+              done();
+            });
+          });
+        });
+      });
+    })
+
+    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){
+              if (err) throw err;
+              assert(!req.session, 'req.session after destroy');
+              res.end();
+            });
+          });
+
+        request(app)
+        .get('/')
+        .end(function(err, res){
+          res.headers.should.not.have.property('set-cookie');
+          done();
+        });
+      })
+    })
+
+    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;
+            req.session.regenerate(function(err){
+              if (err) throw err;
+              id.should.not.equal(req.session.id);
+              res.end();
+            });
+          });
+
+        request(app)
+        .get('/')
+        .end(function(err, res){
+          var id = sid(res);
+
+          request(app)
+          .get('/')
+          .set('Cookie', 'connect.sid=' + id)
+          .end(function(err, res){
+            sid(res).should.not.equal('');
+            sid(res).should.not.equal(id);
+            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;
+              req.session.cookie.secure = true;
+              res.end();
+            });
+
+          request(app)
+          .get('/')
+          .set('X-Forwarded-Proto', 'https')
+          .expect(200, function(err, res){
+            if (err) return done(err);
+            var val = cookie(res);
+            should(val).not.containEql('HttpOnly');
+            should(val).containEql('Secure');
+            done();
+          });
+        })
+
+        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();
+            });
+
+          request(app)
+          .get('/admin')
+          .expect(200, function(err, res){
+            if (err) return done(err);
+            var val = cookie(res);
+            should(val).not.containEql('Expires');
+            done();
+          });
+        })
+
+        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();
+            });
+
+          request(app)
+          .get('/admin/foo')
+          .end(function(err, res){
+            res.headers.should.have.property('set-cookie');
+
+            request(app)
+            .get('/admin')
+            .set('Cookie', 'connect.sid=' + sid(res))
+            .end(function(err, res){
+              res.headers.should.not.have.property('set-cookie');
+              done();
+            })
+          });
+        })
+
+        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;
+              res.end();
+            });
+
+          request(app)
+          .get('/admin')
+          .expect(200, function(err, res){
+            if (err) return done(err);
+            var val = cookie(res);
+            should(val).not.containEql('HttpOnly');
+            should(val).not.containEql('Secure');
+            should(val).containEql('Path=/admin');
+            should(val).containEql('Expires');
+            done();
+          });
+        })
+
+        it('should preserve cookies set before writeHead is called', function(done){
+          function getPreviousCookie(res) {
+            var val = res.headers['set-cookie'];
+            if (!val) return '';
+            return /previous=([^;]+);/.exec(val[0])[1];
+          }
+
+          var app = express()
+            .use(cookieParser('keyboard cat'))
+            .use(session())
+            .use(function(req, res, next){
+              var cookie = new Cookie();
+              res.setHeader('Set-Cookie', cookie.serialize('previous', 'cookieValue'));
+              res.end();
+            });
+
+          request(app)
+          .get('/')
+          .end(function(err, res){
+            getPreviousCookie(res).should.equal('cookieValue');
+            done();
+          });
+        })
+      })
+
+      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;
+              res.end();
+            });
+
+          request(app)
+          .get('/')
+          .end(function(err, res){
+            res.headers.should.not.have.property('set-cookie');
+            done();
+          });
+        })
+      })
+
+      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) {
+                return res.end();
+              }
+              req.session.foo = Math.random();
+              res.end();
+            });
+
+          request(app)
+          .get('/')
+          .end(function(err, res){
+            res.status.should.equal(200);
+            res.headers.should.not.have.property('set-cookie');
+            done();
+          });
+        })
+
+        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) {
+                return res.end();
+              }
+
+              req.session.foo = Math.random();
+              res.end();
+            });
+
+          request(app)
+          .get('/')
+          .set('host', 'http://foo/bar')
+          .end(function(err, res){
+            res.status.should.equal(200);
+            res.headers.should.not.have.property('set-cookie');
+            done();
+          });
+        })
+      })
+
+      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();
+              res.end();
+            });
+
+          request(app)
+          .get('/foo/bar/baz')
+          .end(function(err, res){
+            res.headers.should.have.property('set-cookie');
+            done();
+          });
+        })
+
+        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();
+              res.end();
+            });
+
+          request(app)
+          .get('/foo/bar/baz')
+          .set('host', 'http://example.com')
+          .end(function(err, res){
+            res.status.should.equal(200);
+            res.headers.should.have.property('set-cookie');
+            done();
+          });
+        })
+      })
+
+      describe('.maxAge', function(){
+        var id;
+        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;
+            req.session.count++;
+            if (req.session.count == 2) req.session.cookie.maxAge = 5000;
+            if (req.session.count == 3) req.session.cookie.maxAge = 3000000000;
+            res.end(req.session.count.toString());
+          });
+
+        it('should set relative in milliseconds', function(done){
+          request(app)
+          .get('/')
+          .end(function(err, res){
+            var a = new Date(expires(res))
+              , b = new Date;
+
+            id = sid(res);
+
+            a.getYear().should.equal(b.getYear());
+            a.getMonth().should.equal(b.getMonth());
+            a.getDate().should.equal(b.getDate());
+            a.getSeconds().should.not.equal(b.getSeconds());
+            var delta = a.valueOf() - b.valueOf();
+            (delta > 1000 && delta < 2000).should.be.ok;
+            res.text.should.equal('1');
+            done();
+          });
+        });
+
+        it('should modify cookie when changed', function(done){
+          request(app)
+          .get('/')
+          .set('Cookie', 'connect.sid=' + id)
+          .end(function(err, res){
+            var a = new Date(expires(res))
+              , b = new Date;
+
+            id = sid(res);
+
+            a.getYear().should.equal(b.getYear());
+            a.getMonth().should.equal(b.getMonth());
+            a.getSeconds().should.not.equal(b.getSeconds());
+            var delta = a.valueOf() - b.valueOf();
+            (delta > 4000 && delta < 5000).should.be.ok;
+            res.text.should.equal('2');
+            done();
+          });
+        });
+
+        it('should modify cookie when changed to large value', function(done){
+          request(app)
+          .get('/')
+          .set('Cookie', 'connect.sid=' + id)
+          .end(function(err, res){
+            var a = new Date(expires(res))
+              , b = new Date;
+
+            id = sid(res);
+
+            var delta = a.valueOf() - b.valueOf();
+            (delta > 2999999000 && delta < 3000000000).should.be.ok;
+            res.text.should.equal('3');
+            done();
+          });
+        });
+      })
+
+      describe('.expires', 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);
+                res.end();
+              });
+
+            request(app)
+            .get('/')
+            .end(function(err, res){
+              expires(res).should.equal('Thu, 01 Jan 1970 00:00:00 GMT');
+              done();
+            });
+          })
+        })
+
+        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;
+                res.end();
+              });
+
+            request(app)
+            .get('/')
+            .expect(200, function(err, res){
+              if (err) return done(err);
+              var val = cookie(res);
+              should(val).not.containEql('Expires=');
+              done();
+            });
+          })
+        })
+      })
+    })
+
+    it('should support req.signedCookies', function(done){
+      var app = express()
+        .use(cookieParser('keyboard cat'))
+        .use(session())
+        .use(function(req, res, next){
+          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');
+
+        request(app)
+        .get('/')
+        .set('Cookie', 'connect.sid=' + sid(res))
+        .end(function(err, res){
+          res.text.should.equal('2');
+          done();
+        });
+      });
+    })
+  })
+})
+
+function cookie(res) {
+  var setCookie = res.headers['set-cookie'];
+  return (setCookie && setCookie[0]) || undefined;
+}
+
+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;
+}

-- 
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