diff --git a/README.md b/README.md index fe5076d8..275b2efb 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,11 @@ req.session.reload(function(err) { }) ``` +#### Session.load(sid, callback) + +Loads the the session data for the given `sid` from the store and re-populates the +`req.session` object. Once complete, the `callback` will be invoked. + #### Session.save(callback) Save the session back to the store, replacing the contents on the store with the @@ -441,7 +446,7 @@ For an example implementation view the [connect-redis](http://github.com/visionm This optional method is used to get all sessions in the store as an array. The `callback` should be called as `callback(error, sessions)`. -### store.destroy(sid, callback) +### store.destroy(sid, [req], callback) **Required** @@ -449,6 +454,8 @@ This required method is used to destroy/delete a session from the store given a session ID (`sid`). The `callback` should be called as `callback(error)` once the session is destroyed. +The request is passed into the `req` argument only if the store sets `store.passReq` to `true`. + ### store.clear(callback) **Optional** @@ -463,7 +470,7 @@ This optional method is used to delete all sessions from the store. The This optional method is used to get the count of all sessions in the store. The `callback` should be called as `callback(error, len)`. -### store.get(sid, callback) +### store.get(sid, [req], callback) **Required** @@ -474,7 +481,9 @@ The `session` argument should be a session if found, otherwise `null` or `undefined` if the session was not found (and there was no error). A special case is made when `error.code === 'ENOENT'` to act like `callback(null, null)`. -### store.set(sid, session, callback) +The request is passed into the `req` argument only if the store sets `store.passReq` to `true`. + +### store.set(sid, session, [req], callback) **Required** @@ -482,7 +491,9 @@ This required method is used to upsert a session into the store given a session ID (`sid`) and session (`session`) object. The callback should be called as `callback(error)` once the session has been set in the store. -### store.touch(sid, session, callback) +The request is passed into the `req` argument only if the store sets `store.passReq` to `true`. + +### store.touch(sid, session, [req], callback) **Recommended** @@ -494,6 +505,17 @@ This is primarily used when the store will automatically delete idle sessions and this method is used to signal to the store the given session is active, potentially resetting the idle timer. +The request is passed into the `req` argument only if the store sets `store.passReq` to `true`. + +### store.passReq + +**Optional** + +A property determining whether the request is passed to the store in the other methods. +Defaults to `false` if left undefined. + +A store wishing to operate in either mode (in order to maintain compatibility with earlier versions of this package) may set this to `true`, but check the arguments given to each method. + ## Compatible Session Stores The following modules implement a session store that is compatible with this diff --git a/index.js b/index.js index 9615346c..774a4863 100644 --- a/index.js +++ b/index.js @@ -98,7 +98,6 @@ function session(options) { // get the session store var store = opts.store || new MemoryStore() - // get the trust proxy setting var trustProxy = opts.proxy @@ -201,6 +200,27 @@ function session(options) { return; } + /** + * Load a `Session` instance via the given `sid` + * and invoke the callback `fn(err, sess)`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + store.load = function(sid, fn){ + var getCallback = function(err, sess){ + if (err) return fn(err); + if (!sess) return fn(); + fn(null, store.createSession(req, sess)) + }; + if (store.passReq) { + store.get(sid, req, getCallback); + } else { + store.get(sid, getCallback); + } + }; + // backwards compatibility for signed cookies // req.secret is passed from the cookie parser middleware var secrets = secret || [req.secret]; @@ -303,14 +323,20 @@ function session(options) { if (shouldDestroy(req)) { // destroy session debug('destroying'); - store.destroy(req.sessionID, function ondestroy(err) { + var ondestroy = function(err) { if (err) { defer(next, err); } debug('destroyed'); writeend(); - }); + } + + if (store.passReq) { + store.destroy(req.sessionID, req, ondestroy); + } else { + store.destroy(req.sessionID, ondestroy); + } return writetop(); } @@ -340,14 +366,20 @@ function session(options) { } else if (storeImplementsTouch && shouldTouch(req)) { // store implements touch method debug('touching'); - store.touch(req.sessionID, req.session, function ontouch(err) { + var ontouch = function(err) { if (err) { defer(next, err); } debug('touched'); writeend(); - }); + } + + if (store.passReq) { + store.touch(req.sessionID, req.session, req, ontouch); + } else { + store.touch(req.sessionID, req.session, ontouch); + } return writetop(); } @@ -478,7 +510,7 @@ function session(options) { // generate the session object debug('fetching %s', req.sessionID); - store.get(req.sessionID, function(err, sess){ + var getHandler = function(err, sess){ // error handling if (err && err.code !== 'ENOENT') { debug('error %j', err); @@ -500,7 +532,12 @@ function session(options) { } next() - }); + } + if (store.passReq) { + store.get(req.sessionID, req, getHandler); + } else { + store.get(req.sessionID, getHandler); + } }; }; diff --git a/session/session.js b/session/session.js index fee7608c..4f0dc4c7 100644 --- a/session/session.js +++ b/session/session.js @@ -69,7 +69,11 @@ defineMethod(Session.prototype, 'resetMaxAge', function resetMaxAge() { */ defineMethod(Session.prototype, 'save', function save(fn) { - this.req.sessionStore.set(this.id, this, fn || function(){}); + if (this.req.sessionStore.passReq) { + this.req.sessionStore.set(this.id, this, this.req, fn || function(){}); + } else { + this.req.sessionStore.set(this.id, this, fn || function(){}); + } return this; }); @@ -89,12 +93,17 @@ defineMethod(Session.prototype, 'reload', function reload(fn) { var req = this.req var store = this.req.sessionStore - store.get(this.id, function(err, sess){ + var getCallback = function(err, sess){ if (err) return fn(err); if (!sess) return fn(new Error('failed to load session')); store.createSession(req, sess); fn(); - }); + }; + if (store.passReq) { + store.get(this.id, req, getCallback); + } else { + store.get(this.id, getCallback); + } return this; }); @@ -108,7 +117,11 @@ defineMethod(Session.prototype, 'reload', function reload(fn) { defineMethod(Session.prototype, 'destroy', function destroy(fn) { delete this.req.session; - this.req.sessionStore.destroy(this.id, fn); + if (this.req.sessionStore.passReq) { + this.req.sessionStore.destroy(this.id, this.req, fn); + } else { + this.req.sessionStore.destroy(this.id, fn); + } return this; }); diff --git a/session/store.js b/session/store.js index 3793877e..b0823da1 100644 --- a/session/store.js +++ b/session/store.js @@ -49,29 +49,15 @@ util.inherits(Store, EventEmitter) Store.prototype.regenerate = function(req, fn){ var self = this; - this.destroy(req.sessionID, function(err){ + var destroyCallback = 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 }; - fn(null, self.createSession(req, sess)) - }); + }; + if (self.passReq) { + this.destroy(req.sessionID, req, destroyCallback); + } else { + this.destroy(req.sessionID, destroyCallback); + } }; /** diff --git a/test/cookie.js b/test/cookie.js index 65ae1fc3..51abb551 100644 --- a/test/cookie.js +++ b/test/cookie.js @@ -57,11 +57,12 @@ describe('new Cookie()', function () { }) it('should set maxAge', function () { - var expires = new Date(Date.now() + 60000) + var now = Date.now() + var expires = new Date(now + 60000) var cookie = new Cookie({ expires: expires }) - assert.ok(expires.getTime() - Date.now() - 1000 <= cookie.maxAge) - assert.ok(expires.getTime() - Date.now() + 1000 >= cookie.maxAge) + assert.ok(expires.getTime() - now - 1000 <= cookie.maxAge) + assert.ok(expires.getTime() - now + 1000 >= cookie.maxAge) }) }) diff --git a/test/session.js b/test/session.js index f0b60fdf..0260837a 100644 --- a/test/session.js +++ b/test/session.js @@ -10,6 +10,7 @@ var request = require('supertest') var session = require('../') var SmartStore = require('./support/smart-store') var SyncStore = require('./support/sync-store') +var ReqStore = require('./support/req-store') var utils = require('./support/utils') var Cookie = require('../session/cookie') @@ -1581,6 +1582,21 @@ describe('session()', function(){ .expect(shouldNotHaveHeader('Set-Cookie')) .expect(200, 'undefined', done) }) + + it('should destroy the previous session when a request is passed', function(done){ + var store = new ReqStore() + var server = createServer({ store: store }, function (req, res) { + req.session.destroy(function (err) { + if (err) res.statusCode = 500 + res.end(String(req.session)) + }) + }) + + request(server) + .get('/') + .expect(shouldNotHaveHeader('Set-Cookie')) + .expect(200, 'undefined', done) + }) }) describe('.regenerate()', function(){ @@ -1606,6 +1622,30 @@ describe('session()', function(){ .expect(200, 'false', done) }); }) + + it('should destroy/replace the previous session when request is passed', function(done){ + var store = new ReqStore() + var server = createServer({ store: store }, function (req, res) { + var id = req.session.id + req.session.regenerate(function (err) { + if (err) res.statusCode = 500 + res.end(String(req.session.id === id)) + }) + }) + + request(server) + .get('/') + .expect(shouldSetCookie('connect.sid')) + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(shouldSetCookie('connect.sid')) + .expect(shouldSetCookieToDifferentSessionId(sid(res))) + .expect(200, 'false', done) + }); + }) }) describe('.reload()', function () { @@ -1649,6 +1689,47 @@ describe('session()', function(){ }) }) + it('should reload session from store when request is passed', function (done) { + var store = new ReqStore() + var server = createServer({ store: store }, 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) { @@ -2177,6 +2258,86 @@ describe('session()', function(){ }) }) + describe('request supporting store', function(){ + it('should respond correctly on save', function(done){ + var store = new ReqStore() + 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 touch on unmodified session', function (done) { + var store = new ReqStore() + var server = createServer({ store: store, resave: false }, function (req, res) { + req.session.user = 'bob' + res.end() + }) + + request(server) + .get('/') + .expect(200, function (err, res) { + if (err) return done(err) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, done) + }) + }) + + it('should respond correctly on destroy', function(done){ + var store = new ReqStore() + 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) + }) + }) + + it('should persist', function(done){ + var store = new ReqStore() + 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) + assert.ok(sess) + request(server) + .get('/') + .set('Cookie', cookie(res)) + .expect(200, 'hits: 2', done) + }) + }) + }) + }) + describe('cookieParser()', function () { it('should read from req.cookies', function(done){ var app = express() diff --git a/test/support/req-store.js b/test/support/req-store.js new file mode 100644 index 00000000..457d18a2 --- /dev/null +++ b/test/support/req-store.js @@ -0,0 +1,31 @@ +'use strict' + +var session = require('../../') +var util = require('util') + +module.exports = ReqStore + +function ReqStore () { + session.Store.call(this) + this.sessions = Object.create(null) +} + +util.inherits(ReqStore, session.Store) + +ReqStore.prototype.passReq = true + +ReqStore.prototype.destroy = function destroy (sid, req, callback) { + delete this.sessions[req.hostname + ' ' + sid] + callback() +} + +ReqStore.prototype.get = function get (sid, req, callback) { + callback(null, JSON.parse(this.sessions[req.hostname + ' ' + sid])) +} + +ReqStore.prototype.set = function set (sid, sess, req, callback) { + this.sessions[req.hostname + ' ' + sid] = JSON.stringify(sess) + callback() +} + +ReqStore.prototype.touch = ReqStore.prototype.set