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