[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