Skip to content

Commit

Permalink
Rewrite MongoStore initialization
Browse files Browse the repository at this point in the history
* Re-use existing or upcoming mongoose connection
* Re-use existing or upcoming node-mongodb-native connection
* Accept full-featured MongoDB connection strings + advanced options
* Compatible with legacy config
* Replace callback by `connected` event
* Add debug

Fix #51, #58, #62, #66, #70, #85, #94, #96, #115, #117, #120
Fix #124, #128, #129, #130, #131, #133, #134
  • Loading branch information
jdesboeufs committed Dec 24, 2014
1 parent d19b127 commit ca40b78
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 271 deletions.
8 changes: 7 additions & 1 deletion History.md
@@ -1,9 +1,15 @@
0.5.0 / In development
==================

* [BREAKING] `auto_reconnect` option is now `true` by default
* Accept full-featured [MongoDB connection strings](http://docs.mongodb.org/manual/reference/connection-string/) as `url` + [advanced options](http://mongodb.github.io/node-mongodb-native/1.4/driver-articles/mongoclient.html)
* Re-use existing or upcoming mongoose connection
* [BREAKING] `auto_reconnect` option is now `true` by default (legacy)
* [BREAKING] Pass `collection` option in `url` in not possible any more
* [BREAKING] Replace for-testing-purpose `callback` by `connected` event
* Add debug (use with `DEBUG=connect-mongo`)
* Improve error management
* Compatibility with `mongodb` `>= 1.2.0` and `< 2.0.0`
* Fix many bugs


0.4.2 / 2014-12-18
Expand Down
245 changes: 119 additions & 126 deletions lib/connect-mongo.js
Expand Up @@ -7,26 +7,27 @@
/**
* Module dependencies
*/

var crypto = require('crypto');
var mongo = require('mongodb');
var url = require('url');
var util = require('util');
var debug = require('debug')('connect-mongo');

var MongoClient = mongo.MongoClient;
var Db = mongo.Db;


/**
* Default options
*/

var defaultOptions = {host: '127.0.0.1',
port: 27017,
collection: 'sessions',
auto_reconnect: true,
ssl: false,
w: 1,
defaultExpirationTime: 1000 * 60 * 60 * 24 * 14
};
var defaultOptions = {
host: '127.0.0.1',
port: 27017,
collection: 'sessions',
auto_reconnect: true,
ssl: false,
w: 1,
defaultExpirationTime: 1000 * 60 * 60 * 24 * 14 // 14 days
};

function defaultSerializer (session) {
// Copy each property of the session to a new object
Expand All @@ -53,157 +54,149 @@ module.exports = function(connect) {

/**
* Initialize MongoStore with the given `options`.
* Calls `readyCallback` when db connection is ready (mainly for testing purposes).
*
* @param {Object} options
* @param {Function} readyCallback
* @api public
*/

function MongoStore(options, readyCallback) {
function MongoStore(options) {
options = options || {};
if(options.hash){
var collectionName = options.collection || defaultOptions.collection;

Store.call(this, options);

// Hash sid
if (options.hash) {
var defaultSalt = 'connect-mongo';
var defaultAlgorithm = 'sha1';
this.hash = {};
this.hash.salt = options.hash.salt ? options.hash.salt : defaultSalt;
this.hash.algorithm = options.hash.algorithm ? options.hash.algorithm : defaultAlgorithm;
}
Store.call(this, options);

if(options.url) {
var db_url = url.parse(options.url);

if (db_url.port) {
options.port = parseInt(db_url.port);
}

if (db_url.pathname) {
var pathname = db_url.pathname.split('/');

if (pathname.length >= 2 && pathname[1]) {
options.db = pathname[1];
}

if (pathname.length >= 3 && pathname[2]) {
options.collection = pathname[2];
}
}
// Serialization
if (options.stringify || (!('stringify' in options) && !('serialize' in options) && !('unserialize' in options))) {
this._serialize_session = JSON.stringify;
this._unserialize_session = JSON.parse;
} else {
this._serialize_session = options.serialize || defaultSerializer;
this._unserialize_session = options.unserialize || identity;
}

if (db_url.hostname) {
options.host = db_url.hostname;
}
// Expiration time
this.defaultExpirationTime = options.defaultExpirationTime || defaultOptions.defaultExpirationTime;

if (db_url.auth) {
var auth = db_url.auth.split(':');
var self = this;

if (auth.length >= 1) {
options.username = auth[0];
}
function changeState(newState) {
debug('switched to state: %s', newState);
self.state = newState;
self.emit(newState);
}

if (auth.length >= 2) {
options.password = auth[1];
}
function connectionReady(err) {
if (err) {
debug('not able to connect to the database');
changeState('disconnected');
throw err;
}
self.collection = self.db.collection(collectionName);
self.collection.ensureIndex({ expires: 1 }, { expireAfterSeconds: 0 }, function (err) {
if (err) throw err;
changeState('connected');
});
}

if (options.mongoose_connection){
if (options.mongoose_connection.user && options.mongoose_connection.pass) {
options.username = options.mongoose_connection.user;
options.password = options.mongoose_connection.pass;
function buildUrlFromOptions() {
if(!options.db || typeof options.db !== 'string') {
throw new Error('Required MongoStore option `db` missing');
}

this.db = new mongo.Db(options.mongoose_connection.db.databaseName,
new mongo.Server(options.mongoose_connection.db.serverConfig.host,
options.mongoose_connection.db.serverConfig.port,
options.mongoose_connection.db.serverConfig.options
),
{ w: options.w || defaultOptions.w });
options.url = 'mongodb://';

} else {
if(!options.db) {
throw new Error('Required MongoStore option `db` missing');
if (options.username) {
options.url += options.username;
if (options.password) options.url += ':' + options.password;

This comment has been minimized.

Copy link
@dlongley

dlongley Dec 24, 2014

Does this work with a blank (empty string) password? Or does it need to be if ('password' in options)?

This comment has been minimized.

Copy link
@jdesboeufs

jdesboeufs Dec 24, 2014

Author Owner

Yes, you will have a connection string like that: mongodb://test:@localhost....
After mongodb.MongoClient url parsing, you will have { username: 'test', password: '' } which is the expected result.

This comment has been minimized.

Copy link
@dlongley

dlongley Dec 24, 2014

This looks like it would produce mongodb://test@localhost... (note: no colon :) as the conditional would be false for a blank password.

This comment has been minimized.

Copy link
@jdesboeufs

jdesboeufs Dec 24, 2014

Author Owner

You're right! Fix is coming.

This comment has been minimized.

Copy link
@dlongley

dlongley Dec 24, 2014

Great, thanks! I was just checking: https://github.com/mongodb/node-mongodb-native/blob/2.0/lib/url_parser.js#L45-L56 ... which does indicate the colon is required to be present to get the password (and the auth info in general).

options.url += '@';
}

if (typeof options.db === 'object') {
this.db = options.db; // Assume it's an instantiated DB Object
} else {
options.url += options.host || defaultOptions.host;
options.url += ':' + (options.port || defaultOptions.port);
options.url += '/' + options.db;

var serverOptions = options.server || {};
serverOptions.auto_reconnect = serverOptions.auto_reconnect || options.auto_reconnect || defaultOptions.auto_reconnect;
serverOptions.ssl = serverOptions.ssl || options.ssl || defaultOptions.ssl;
if (options.ssl || defaultOptions.ssl) options.url += '?ssl=true';

this.db = new mongo.Db(options.db,
new mongo.Server(options.host || defaultOptions.host,
options.port || defaultOptions.port,
serverOptions),
{ w: options.w || defaultOptions.w });
if (!options.mongoOptions) {
options.mongoOptions = {
server: { auto_reconnect: options.auto_reconnect || defaultOptions.auto_reconnect },
db: { w: options.w || defaultOptions.w }
};
}
}

this.db_collection_name = options.collection || defaultOptions.collection;
function initWithUrl() {
MongoClient.connect(options.url, options.mongoOptions || {}, function(err, db) {
if (!err) self.db = db;
connectionReady(err);
});
}

if (options.stringify || (!('stringify' in options) && !('serialize' in options) && !('unserialize' in options))) {
this._serialize_session = JSON.stringify;
this._unserialize_session = JSON.parse;
} else {
this._serialize_session = options.serialize || defaultSerializer;
this._unserialize_session = options.unserialize || identity;
function initWithMongooseConnection() {
if (options.mongoose_connection.readyState === 1) {
self.db = options.mongoose_connection.db;
process.nextTick(connectionReady);
} else {
options.mongoose_connection.once('open', function() {
self.db = options.mongoose_connection.db;
connectionReady();
});
}
}

var self = this;
function initWithNativeDb() {
self.db = options.db;

this._get_collection = function(callback) {
if (self.collection) {
callback(null, self.collection);
} else if (self.db.openCalled) {
self.db.collection(self.db_collection_name, function(err, collection) {
if (err) {
debug('not able to get collection: ' + self.db_collection_name);
return callback(err);
} else {
self.collection = collection;

// Make sure we have a TTL index on "expires", so mongod will automatically
// remove expired sessions. expireAfterSeconds is set to 0 because we want
// mongo to remove anything expired without any additional delay.
self.collection.ensureIndex({expires: 1}, {expireAfterSeconds: 0}, function(err) {
if (err) {
debug('not able to set TTL index on collection: ' + self.db_collection_name);
return callback(err);
}

callback(null, self.collection);
});
}
});
if (options.db.openCalled) {
options.db.collection(collectionName, connectionReady);
} else {
self._open_database(callback);
options.db.open(connectionReady);
}
};

this._open_database = function(cb){
self.db.open(function(err, db) {
if (err) {
if (!(err instanceof Error)) { err = new Error(String(err)); }
err.message = 'Error connecting to database: ' + err.message;
debug('not able to connect to database');
return cb(err);
}
}

if (options.username && options.password) {
db.authenticate(options.username, options.password, function () {
self._get_collection(cb);
this.getCollection = function (done) {
switch (self.state) {
case 'connected':
done(null, self.collection);
break;
case 'connecting':
self.once('connected', function () {
done(null, self.collection);
});
} else {
self._get_collection(cb);
}
});
break;
case 'disconnected':
done(new Error('Not connected'));
break;
}
};

this.defaultExpirationTime = options.defaultExpirationTime || defaultOptions.defaultExpirationTime;
changeState('init');

if (options.url) {
debug('use strategy: `url`');
initWithUrl();
} else if (options.mongoose_connection) {
debug('use strategy: `mongoose_connection`');
initWithMongooseConnection();
} else if (options.db && options.db instanceof Db) {
debug('use strategy: `native_db`');
initWithNativeDb();
} else {
debug('use strategy: `legacy`');
buildUrlFromOptions();
initWithUrl();
}

if (readyCallback) this._open_database(readyCallback);
changeState('connecting');

}

Expand All @@ -223,7 +216,7 @@ module.exports = function(connect) {
MongoStore.prototype.get = function(sid, callback) {
sid = this.hash ? crypto.createHash(this.hash.algorithm).update(this.hash.salt + sid).digest('hex') : sid;
var self = this;
this._get_collection(function(err, collection) {
this.getCollection(function(err, collection) {
if (err) return callback(err);
collection.findOne({_id: sid}, function(err, session) {
if (err) {
Expand Down Expand Up @@ -282,7 +275,7 @@ module.exports = function(connect) {
s.expires = new Date(today.getTime() + this.defaultExpirationTime);
}

this._get_collection(function(err, collection) {
this.getCollection(function(err, collection) {
if (err) return callback(err);
collection.update({_id: sid}, s, {upsert: true, safe: true}, function(err) {
if (err) debug('not able to set/update session: ' + sid);
Expand All @@ -301,7 +294,7 @@ module.exports = function(connect) {

MongoStore.prototype.destroy = function(sid, callback) {
sid = this.hash ? crypto.createHash(this.hash.algorithm).update(this.hash.salt + sid).digest('hex') : sid;
this._get_collection(function(err, collection) {
this.getCollection(function(err, collection) {
if (err) return callback(err);
collection.remove({_id: sid}, function(err) {
if (err) debug('not able to destroy session: ' + sid);
Expand All @@ -318,7 +311,7 @@ module.exports = function(connect) {
*/

MongoStore.prototype.length = function(callback) {
this._get_collection(function(err, collection) {
this.getCollection(function(err, collection) {
if (err) return callback(err);
collection.count({}, function(err, count) {
if (err) debug('not able to count sessions');
Expand All @@ -335,7 +328,7 @@ module.exports = function(connect) {
*/

MongoStore.prototype.clear = function(callback) {
this._get_collection(function(err, collection) {
this.getCollection(function(err, collection) {
if (err) return callback(err);
collection.drop(function(err) {
if (err) debug('not able to clear sessions');
Expand Down
4 changes: 2 additions & 2 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "connect-mongo",
"version": "0.4.2",
"version": "0.5.0",
"description": "MongoDB session store for Connect",
"keywords": [
"connect",
Expand All @@ -24,7 +24,7 @@
"devDependencies": {
"connect": "2.27.6",
"mocha": "1.x",
"mongoose": ">= 2.6.x"
"mongoose": ">= 2.6.0 < 4.0.0"
},
"scripts": {
"test": "make test"
Expand Down

0 comments on commit ca40b78

Please sign in to comment.