From 171d8b6635d9ba27b0aab64355805bdf41344a45 Mon Sep 17 00:00:00 2001 From: Douglas Christopher Wilson Date: Fri, 29 Jan 2016 13:26:00 -0500 Subject: [PATCH 1/7] 1.13.1 --- HISTORY.md | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 727834eb..ffb63d71 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,5 @@ -unreleased -========== +1.13.1 / 2016-01-29 +=================== * deps: parseurl@~1.3.1 - perf: enable strict mode diff --git a/package.json b/package.json index 8b945f2e..b9e15281 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "express-session", - "version": "1.13.0", + "version": "1.13.1", "description": "Simple session middleware for Express", "author": "TJ Holowaychuk (http://tjholowaychuk.com)", "contributors": [ From 59b1f56097cac572ba587db89aa12e0794611923 Mon Sep 17 00:00:00 2001 From: Cian Clarke Date: Wed, 2 Mar 2016 08:54:13 -0500 Subject: [PATCH 2/7] docs: fix session store list for CommonMark spec fixes #248 closes #280 --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index f81381f2..f2a6df6b 100644 --- a/README.md +++ b/README.md @@ -412,90 +412,111 @@ The following modules implement a session store that is compatible with this module. Please make a PR to add additional modules :) [![★][cassandra-store-image] cassandra-store][cassandra-store-url] An Apache Cassandra-based session store. + [cassandra-store-url]: https://www.npmjs.com/package/cassandra-store [cassandra-store-image]: https://img.shields.io/github/stars/webcc/cassandra-store.svg?label=%E2%98%85 [![★][cluster-store-image] cluster-store][cluster-store-url] A wrapper for using in-process / embedded stores - such as SQLite (via knex), leveldb, files, or memory - with node cluster (desirable for Raspberry Pi 2 and other multi-core embedded devices). + [cluster-store-url]: https://www.npmjs.com/package/cluster-store [cluster-store-image]: https://img.shields.io/github/stars/coolaj86/cluster-store.svg?label=%E2%98%85 [![★][connect-couchbase-image] connect-couchbase][connect-couchbase-url] A [couchbase](http://www.couchbase.com/)-based session store. + [connect-couchbase-url]: https://www.npmjs.com/package/connect-couchbase [connect-couchbase-image]: https://img.shields.io/github/stars/christophermina/connect-couchbase.svg?label=%E2%98%85 [![★][connect-dynamodb-image] connect-dynamodb][connect-dynamodb-url] A DynamoDB-based session store. + [connect-dynamodb-url]: https://github.com/ca98am79/connect-dynamodb [connect-dynamodb-image]: https://img.shields.io/github/stars/ca98am79/connect-dynamodb.svg?label=%E2%98%85 [![★][connect-mssql-image] connect-mssql][connect-mssql-url] A SQL Server-based session store. + [connect-mssql-url]: https://www.npmjs.com/package/connect-mssql [connect-mssql-image]: https://img.shields.io/github/stars/patriksimek/connect-mssql.svg?label=%E2%98%85 [![★][connect-monetdb-image] connect-monetdb][connect-monetdb-url] A MonetDB-based session store. + [connect-monetdb-url]: https://www.npmjs.com/package/connect-monetdb [connect-monetdb-image]: https://img.shields.io/github/stars/MonetDB/npm-connect-monetdb.svg?label=%E2%98%85 [![★][connect-mongo-image] connect-mongo][connect-mongo-url] A MongoDB-based session store. + [connect-mongo-url]: https://www.npmjs.com/package/connect-mongo [connect-mongo-image]: https://img.shields.io/github/stars/kcbanner/connect-mongo.svg?label=%E2%98%85 [![★][connect-mongodb-session-image] connect-mongodb-session][connect-mongodb-session-url] Lightweight MongoDB-based session store built and maintained by MongoDB. + [connect-mongodb-session-url]: https://www.npmjs.com/package/connect-mongodb-session [connect-mongodb-session-image]: https://img.shields.io/github/stars/mongodb-js/connect-mongodb-session.svg?label=%E2%98%85 [![★][connect-pg-simple-image] connect-pg-simple][connect-pg-simple-url] A PostgreSQL-based session store. + [connect-pg-simple-url]: https://www.npmjs.com/package/connect-pg-simple [connect-pg-simple-image]: https://img.shields.io/github/stars/voxpelli/node-connect-pg-simple.svg?label=%E2%98%85 [![★][connect-redis-image] connect-redis][connect-redis-url] A Redis-based session store. + [connect-redis-url]: https://www.npmjs.com/package/connect-redis [connect-redis-image]: https://img.shields.io/github/stars/tj/connect-redis.svg?label=%E2%98%85 [![★][connect-memcached-image] connect-memcached][connect-memcached-url] A memcached-based session store. + [connect-memcached-url]: https://www.npmjs.com/package/connect-memcached [connect-memcached-image]: https://img.shields.io/github/stars/balor/connect-memcached.svg?label=%E2%98%85 [![★][connect-session-knex-image] connect-session-knex][connect-session-knex-url] A session store using [Knex.js](http://knexjs.org/), which is a SQL query builder for PostgreSQL, MySQL, MariaDB, SQLite3, and Oracle. + [connect-session-knex-url]: https://www.npmjs.com/package/connect-session-knex [connect-session-knex-image]: https://img.shields.io/github/stars/llambda/connect-session-knex.svg?label=%E2%98%85 [![★][connect-session-sequelize-image] connect-session-sequelize][connect-session-sequelize-url] A session store using [Sequelize.js](http://sequelizejs.com/), which is a Node.js / io.js ORM for PostgreSQL, MySQL, SQLite and MSSQL. + [connect-session-sequelize-url]: https://www.npmjs.com/package/connect-session-sequelize [connect-session-sequelize-image]: https://img.shields.io/github/stars/mweibel/connect-session-sequelize.svg?label=%E2%98%85 [![★][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store. + [connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3 [connect-sqlite3-image]: https://img.shields.io/github/stars/rawberg/connect-sqlite3.svg?label=%E2%98%85 [![★][express-nedb-session-image] express-nedb-session][express-nedb-session-url] A NeDB-based session store. + [express-nedb-session-url]: https://www.npmjs.com/package/express-nedb-session [express-nedb-session-image]: https://img.shields.io/github/stars/louischatriot/express-nedb-session.svg?label=%E2%98%85 [![★][level-session-store-image] level-session-store][level-session-store-url] A LevelDB-based session store. + [level-session-store-url]: https://www.npmjs.com/package/level-session-store [level-session-store-image]: https://img.shields.io/github/stars/scriptollc/level-session-store.svg?label=%E2%98%85 [![★][mssql-session-store-image] mssql-session-store][mssql-session-store-url] A SQL Server-based session store. + [mssql-session-store-url]: https://www.npmjs.com/package/mssql-session-store [mssql-session-store-image]: https://img.shields.io/github/stars/jwathen/mssql-session-store.svg?label=%E2%98%85 [![★][nedb-session-store-image] nedb-session-store][nedb-session-store-url] An alternate NeDB-based (either in-memory or file-persisted) session store. + [nedb-session-store-url]: https://www.npmjs.com/package/nedb-session-store [nedb-session-store-image]: https://img.shields.io/github/stars/JamesMGreene/nedb-session-store.svg?label=%E2%98%85 [![★][sequelstore-connect-image] sequelstore-connect][sequelstore-connect-url] A session store using [Sequelize.js](http://sequelizejs.com/). + [sequelstore-connect-url]: https://www.npmjs.com/package/sequelstore-connect [sequelstore-connect-image]: https://img.shields.io/github/stars/MattMcFarland/sequelstore-connect.svg?label=%E2%98%85 [![★][session-file-store-image] session-file-store][session-file-store-url] A file system-based session store. + [session-file-store-url]: https://www.npmjs.com/package/session-file-store [session-file-store-image]: https://img.shields.io/github/stars/valery-barysok/session-file-store.svg?label=%E2%98%85 [![★][session-rethinkdb-image] session-rethinkdb][session-rethinkdb-url] A [RethinkDB](http://rethinkdb.com/)-based session store. + [session-rethinkdb-url]: https://www.npmjs.com/package/session-rethinkdb [session-rethinkdb-image]: https://img.shields.io/github/stars/llambda/session-rethinkdb.svg?label=%E2%98%85 From db2a442266cd27c9ce5c32287000f201b567e57f Mon Sep 17 00:00:00 2001 From: Mike Goodwin Date: Sun, 6 Mar 2016 23:00:31 +0000 Subject: [PATCH 3/7] docs: add connect-azuretables to list of session stores closes #285 --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f2a6df6b..56e47732 100644 --- a/README.md +++ b/README.md @@ -423,6 +423,11 @@ and other multi-core embedded devices). [cluster-store-url]: https://www.npmjs.com/package/cluster-store [cluster-store-image]: https://img.shields.io/github/stars/coolaj86/cluster-store.svg?label=%E2%98%85 +[![★][connect-azuretables-image] connect-azuretables][connect-azuretables-url] An [Azure Table Storage](https://azure.microsoft.com/en-gb/services/storage/tables/)-based session store. + +[connect-azuretables-url]: https://www.npmjs.com/package/connect-azuretables +[connect-azuretables-image]: https://img.shields.io/github/stars/mike-goodwin/connect-azuretables.svg?label=%E2%98%85 + [![★][connect-couchbase-image] connect-couchbase][connect-couchbase-url] A [couchbase](http://www.couchbase.com/)-based session store. [connect-couchbase-url]: https://www.npmjs.com/package/connect-couchbase From 30a3f498203e0cd3b8815713f29e1f8f10f8c34a Mon Sep 17 00:00:00 2001 From: Gabriel Foust Date: Wed, 2 Mar 2016 14:54:08 -0600 Subject: [PATCH 4/7] Methods are no longer enumerable on req.session object closes #282 --- HISTORY.md | 5 +++++ session/session.js | 39 ++++++++++++++++++++++++++++----------- test/session.js | 18 ++++++++++++++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index ffb63d71..4145b4fd 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,8 @@ +unreleased +========== + + * Methods are no longer enumerable on `req.session` object + 1.13.1 / 2016-01-29 =================== diff --git a/session/session.js b/session/session.js index c3c0f15a..2eacde60 100644 --- a/session/session.js +++ b/session/session.js @@ -44,9 +44,9 @@ function Session(req, data) { * @api public */ -Session.prototype.touch = function(){ +defineMethod(Session.prototype, 'touch', function touch() { return this.resetMaxAge(); -}; +}); /** * Reset `.maxAge` to `.originalMaxAge`. @@ -55,10 +55,10 @@ Session.prototype.touch = function(){ * @api public */ -Session.prototype.resetMaxAge = function(){ +defineMethod(Session.prototype, 'resetMaxAge', function resetMaxAge() { this.cookie.maxAge = this.cookie.originalMaxAge; return this; -}; +}); /** * Save the session data with optional callback `fn(err)`. @@ -68,10 +68,10 @@ Session.prototype.resetMaxAge = function(){ * @api public */ -Session.prototype.save = function(fn){ +defineMethod(Session.prototype, 'save', function save(fn) { this.req.sessionStore.set(this.id, this, fn || function(){}); return this; -}; +}); /** * Re-loads the session data _without_ altering @@ -85,7 +85,7 @@ Session.prototype.save = function(fn){ * @api public */ -Session.prototype.reload = function(fn){ +defineMethod(Session.prototype, 'reload', function reload(fn) { var req = this.req , store = this.req.sessionStore; store.get(this.id, function(err, sess){ @@ -95,7 +95,7 @@ Session.prototype.reload = function(fn){ fn(); }); return this; -}; +}); /** * Destroy `this` session. @@ -105,11 +105,11 @@ Session.prototype.reload = function(fn){ * @api public */ -Session.prototype.destroy = function(fn){ +defineMethod(Session.prototype, 'destroy', function destroy(fn) { delete this.req.session; this.req.sessionStore.destroy(this.id, fn); return this; -}; +}); /** * Regenerate this request's session. @@ -119,7 +119,24 @@ Session.prototype.destroy = function(fn){ * @api public */ -Session.prototype.regenerate = function(fn){ +defineMethod(Session.prototype, 'regenerate', function regenerate(fn) { this.req.sessionStore.regenerate(this.req, fn); return this; +}); + +/** + * Helper function for creating a method on a prototype. + * + * @param {Object} obj + * @param {String} name + * @param {Function} fn + * @private + */ +function defineMethod(obj, name, fn) { + Object.defineProperty(obj, name, { + configurable: true, + enumerable: false, + value: fn, + writable: true + }); }; diff --git a/test/session.js b/test/session.js index 682cb8d0..35f20de2 100644 --- a/test/session.js +++ b/test/session.js @@ -1420,6 +1420,24 @@ describe('session()', function(){ }); }) + it('should not have enumerable methods', function (done) { + var app = express() + .use(session({ secret: 'keyboard cat', cookie: { maxAge: min }})) + .use(function(req, res, next) { + req.session.foo = 'foo'; + req.session.bar = 'bar'; + var keys = []; + for (var key in req.session) { + keys.push(key); + } + res.end(keys.sort().join(',')); + }); + + request(app) + .get('/') + .expect(200, 'bar,cookie,foo', done); + }); + describe('.destroy()', function(){ it('should destroy the previous session', function(done){ var app = express() From 005078aaada837d2a57e7324986ead60bf326e84 Mon Sep 17 00:00:00 2001 From: Charles Hill Date: Tue, 2 Feb 2016 16:11:51 +0100 Subject: [PATCH 5/7] docs: add express-mysql-session to list of session stores closes #268 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 56e47732..ca9da9d1 100644 --- a/README.md +++ b/README.md @@ -485,6 +485,12 @@ and other multi-core embedded devices). [connect-session-sequelize-url]: https://www.npmjs.com/package/connect-session-sequelize [connect-session-sequelize-image]: https://img.shields.io/github/stars/mweibel/connect-session-sequelize.svg?label=%E2%98%85 +[![★][express-mysql-session-image] express-mysql-session][express-mysql-session-url] A session store using native +[MySQL](https://www.mysql.com/) via the [node-mysql](https://github.com/felixge/node-mysql) module. + +[express-mysql-session-url]: https://www.npmjs.com/package/express-mysql-session +[express-mysql-session-image]: https://img.shields.io/github/stars/chill117/express-mysql-session.svg?label=%E2%98%85 + [![★][connect-sqlite3-image] connect-sqlite3][connect-sqlite3-url] A [SQLite3](https://github.com/mapbox/node-sqlite3) session store modeled after the TJ's `connect-redis` store. [connect-sqlite3-url]: https://www.npmjs.com/package/connect-sqlite3 From dfefc9c46c1d7e7f2157e43d89044427f0aa92de Mon Sep 17 00:00:00 2001 From: sboehler Date: Tue, 29 Mar 2016 22:01:28 +0200 Subject: [PATCH 6/7] Ensure the session is touched before the cookie is set --- index.js | 3 ++- test/session.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 0de7c62f..46f22bd0 100644 --- a/index.js +++ b/index.js @@ -202,7 +202,8 @@ function session(options){ if (!shouldSetCookie(req)) { return; } - + // touch session + req.session.touch(); setcookie(res, name, req.sessionID, secrets[0], cookie.data); }); diff --git a/test/session.js b/test/session.js index 35f20de2..4252d5b0 100644 --- a/test/session.js +++ b/test/session.js @@ -862,6 +862,32 @@ describe('session()', function(){ .expect(shouldSetCookie('connect.sid')) .expect(200, done); }); + + it('should have updated the session before setting the cookie', function(done) { + var app = express() + .use(function(req, res, next){ req.secret = 'keyboard cat'; next(); }) + .use(session({ cookie: { maxAge: min }, rolling: true })) + .use(function(req, res) { + var sess = req.session + res.write(''); + setTimeout(function() { res.end(); }, 100); + }); + var agent = request.agent(app); + agent.get('/') + .end(function(err, res) { + var timestamp1 = res.headers['set-cookie'][0].match(/\d\d\:\d\d:\d\d/)[0]; + setTimeout( + /* give 'expires' at least a second to change */ + function() { + agent.get('/') + .end(function(err, res) { + var timestamp2 = res.headers['set-cookie'][0].match(/\d\d\:\d\d:\d\d/)[0]; + assert.notEqual(timestamp1, timestamp2); + done(); + }) + }, 1000) + }); + }) }); describe('resave option', function(){ From 9f6ff067e8a7a28346cc013651aa58880744d751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Silvio=20B=C3=B6hler?= Date: Wed, 30 Mar 2016 14:48:05 +0000 Subject: [PATCH 7/7] Cleanup of the session commit code --- index.js | 213 +++++++++++++++++++++++++++---------------------------- 1 file changed, 106 insertions(+), 107 deletions(-) diff --git a/index.js b/index.js index 46f22bd0..652fa4cb 100644 --- a/index.js +++ b/index.js @@ -184,136 +184,135 @@ function session(options){ // get the session ID from the cookie var cookieId = req.sessionID = getcookie(req, name, secrets); - // set-cookie - onHeaders(res, function(){ - if (!req.session) { - debug('no session'); - return; + var _callback = null; + /** + * Commits the session. + * May be called multiple times and ensures it only runs once, + * Returns true if all commit related tasks are complete when the function returns. + * Returns false it it has async tasks pending. + * If _callback is defined, it is called without arguments once all async tasks are complete. + */ + var commitSession = (function() { + var started = false, + finished = false; + function done() { + finished = true; + if (_callback) { + _callback(); + } } + return function() { + if (started) { + return finished; + } + started = true; + if (shouldDestroy(req)) { + // destroy session + debug('destroying'); + store.destroy(req.sessionID, function ondestroy(err) { + if (err) { + defer(next, err); + } + debug('destroyed'); + done(); + }); + return finished; + } + if (!req.session) { + debug('no session'); + done(); + return finished; + } + // touch session + req.session.touch(); + + // only send secure cookies via https + var cookie = req.session.cookie; + if (cookie.secure && !issecure(req, trustProxy)) { + debug('not secured'); + } else if (shouldSetCookie(req)) { + setcookie(res, name, req.sessionID, secrets[0], cookie.data); + } - var cookie = req.session.cookie; - - // only send secure cookies via https - if (cookie.secure && !issecure(req, trustProxy)) { - debug('not secured'); - return; + if (shouldSave(req)) { + req.session.save(function onsave(err) { + if (err) { + defer(next, err); + } + done(); + }); + } + else if (storeImplementsTouch && shouldTouch(req)) { + // store implements touch method + debug('touching'); + store.touch(req.sessionID, req.session, function ontouch(err) { + if (err) { + defer(next, err); + } + debug('touched'); + done(); + }); + } else { + done(); + } + return finished; } + })(); - if (!shouldSetCookie(req)) { - return; - } - // touch session - req.session.touch(); - setcookie(res, name, req.sessionID, secrets[0], cookie.data); - }); + + // commit the session - can be called before or after res.end() + onHeaders(res, commitSession); // proxy end() to commit the session var _end = res.end; var _write = res.write; var ended = false; + res.end = function end(chunk, encoding) { if (ended) { + // end has already been called return false; } - ended = true; - var ret; - var sync = true; + // commit the session and get the commit status + var committed = commitSession(); - function writeend() { - if (sync) { - ret = _end.call(res, chunk, encoding); - sync = false; - return; - } - - _end.call(res); - } - - function writetop() { - if (!sync) { - return ret; - } - - if (chunk == null) { - ret = true; - return ret; - } - - var contentLength = Number(res.getHeader('Content-Length')); - - if (!isNaN(contentLength) && contentLength > 0) { - // measure chunk - chunk = !Buffer.isBuffer(chunk) - ? new Buffer(chunk, encoding) - : chunk; - encoding = undefined; - - if (chunk.length !== 0) { - debug('split response'); - ret = _write.call(res, chunk.slice(0, chunk.length - 1)); - chunk = chunk.slice(chunk.length - 1, chunk.length); - return ret; - } - } - - ret = _write.call(res, chunk, encoding); - sync = false; - - return ret; + if (committed) { + // all commit activities have completed + return _end.call(res, chunk, encoding); } - if (shouldDestroy(req)) { - // destroy session - debug('destroying'); - store.destroy(req.sessionID, function ondestroy(err) { - if (err) { - defer(next, err); - } - - debug('destroyed'); - writeend(); - }); - - return writetop(); + /* + * commit is still running + */ + + // provide a callback to write data + _callback = function() { + _end.call(res, chunk, encoding); } - // no session to save - if (!req.session) { - debug('no session'); - return _end.call(res, chunk, encoding); + if (chunk == null) { + // nothing to do - wait for commit to call the callback + return; } - // touch session - req.session.touch(); - - if (shouldSave(req)) { - req.session.save(function onsave(err) { - if (err) { - defer(next, err); - } - - writeend(); - }); - - return writetop(); - } else if (storeImplementsTouch && shouldTouch(req)) { - // store implements touch method - debug('touching'); - store.touch(req.sessionID, req.session, function ontouch(err) { - if (err) { - defer(next, err); - } - - debug('touched'); - writeend(); - }); - - return writetop(); + // write as much data as possible while the commit is running + var ret; + var contentLength = Number(res.getHeader('Content-Length')); + if (!isNaN(contentLength) && contentLength > 0 && chunk.length !== 0) { + // send everything but the last character + chunk = !Buffer.isBuffer(chunk) ? new Buffer(chunk, encoding) : chunk; + encoding = undefined; + debug('split response'); + ret = _write.call(res, chunk.slice(0, chunk.length - 1)); + chunk = chunk.slice(chunk.length - 1, chunk.length); + } else { + // send everything and change callback + ret = _write.call(res, chunk, encoding); + _callback = _end.bind(res); } - - return _end.call(res, chunk, encoding); + return ret; }; // generate the session