480 lines
12 KiB
JavaScript
480 lines
12 KiB
JavaScript
/* jshint camelcase: false */
|
|
|
|
/**
|
|
* Module dependencies
|
|
*/
|
|
var _ = require('lodash');
|
|
var crypto = require('crypto');
|
|
var mongo = require('mongodb');
|
|
var util = require('util');
|
|
var debug = require('debug')('connect-mongo');
|
|
var deprecate = require('depd')('connect-mongo');
|
|
|
|
var MongoClient = mongo.MongoClient;
|
|
var Db = mongo.Db;
|
|
|
|
|
|
/**
|
|
* Default options
|
|
*/
|
|
var defaultOptions = {
|
|
// Legacy strategy default options
|
|
host: '127.0.0.1',
|
|
port: 27017,
|
|
autoReconnect: true,
|
|
ssl: false,
|
|
w: 1,
|
|
|
|
// Global options
|
|
collection: 'sessions',
|
|
stringify: true,
|
|
hash: false,
|
|
ttl: 60 * 60 * 24 * 14, // 14 days
|
|
autoRemove: 'native',
|
|
autoRemoveInterval: 10
|
|
};
|
|
|
|
var defaultHashOptions = {
|
|
salt: 'connect-mongo',
|
|
algorithm: 'sha1'
|
|
};
|
|
|
|
var defaultSerializationOptions = {
|
|
serialize: function (session) {
|
|
// Copy each property of the session to a new object
|
|
var obj = {};
|
|
for (var prop in session) {
|
|
if (prop === 'cookie') {
|
|
|
|
// Convert the cookie instance to an object, if possible
|
|
// This gets rid of the duplicate object under session.cookie.data property
|
|
|
|
obj.cookie = session.cookie.toJSON ? session.cookie.toJSON() : session.cookie;
|
|
} else {
|
|
obj[prop] = session[prop];
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
},
|
|
unserialize: _.identity
|
|
};
|
|
|
|
var stringifySerializationOptions = {
|
|
serialize: JSON.stringify,
|
|
unserialize: JSON.parse
|
|
};
|
|
|
|
module.exports = function(connect) {
|
|
var Store = connect.Store || connect.session.Store;
|
|
var MemoryStore = connect.MemoryStore || connect.session.MemoryStore;
|
|
|
|
/**
|
|
* Initialize MongoStore with the given `options`.
|
|
*
|
|
* @param {Object} options
|
|
* @api public
|
|
*/
|
|
|
|
function MongoStore(options) {
|
|
|
|
/* Deprecated options */
|
|
|
|
if ('auto_reconnect' in options) {
|
|
deprecate('auto_reconnect option is deprecated. Use autoReconnect instead');
|
|
options.autoReconnect = options.auto_reconnect;
|
|
}
|
|
|
|
if ('mongoose_connection' in options) {
|
|
deprecate('mongoose_connection option is deprecated. Use mongooseConnection instead');
|
|
options.mongooseConnection = options.mongoose_connection;
|
|
}
|
|
|
|
if ('defaultExpirationTime' in options) {
|
|
deprecate('defaultExpirationTime option is deprecated. Use ttl instead');
|
|
options.ttl = options.defaultExpirationTime / 1000;
|
|
}
|
|
|
|
/* Fallback */
|
|
|
|
if (options.fallbackMemory && MemoryStore) {
|
|
return new MemoryStore();
|
|
}
|
|
|
|
/* Options */
|
|
|
|
options = _.defaults(options || {}, defaultOptions);
|
|
|
|
if (options.hash) {
|
|
options.hash = _.defaults(options.hash, defaultHashOptions);
|
|
}
|
|
|
|
if (!options.stringify || options.serialize || options.unserialize) {
|
|
options = _.defaults(options, defaultSerializationOptions);
|
|
} else {
|
|
options = _.assign(options, stringifySerializationOptions);
|
|
}
|
|
|
|
this.options = options;
|
|
|
|
Store.call(this, options);
|
|
|
|
var self = this;
|
|
|
|
function changeState(newState) {
|
|
debug('switched to state: %s', newState);
|
|
self.state = newState;
|
|
self.emit(newState);
|
|
}
|
|
|
|
function connectionReady(err) {
|
|
if (err) {
|
|
debug('not able to connect to the database');
|
|
changeState('disconnected');
|
|
throw err;
|
|
}
|
|
|
|
self.collection = self.db.collection(options.collection);
|
|
|
|
switch (options.autoRemove) {
|
|
|
|
case 'native':
|
|
self.collection.ensureIndex({ expires: 1 }, { expireAfterSeconds: 0 }, function (err) {
|
|
if (err) throw err;
|
|
changeState('connected');
|
|
});
|
|
break;
|
|
|
|
case 'interval':
|
|
setInterval(function () {
|
|
self.collection.remove({ expires: { $lt: new Date() } }, { w: 0 });
|
|
}, options.autoRemoveInterval * 1000 * 60);
|
|
changeState('connected');
|
|
break;
|
|
|
|
default:
|
|
changeState('connected');
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
function buildUrlFromOptions() {
|
|
if(!options.db || typeof options.db !== 'string') {
|
|
throw new Error('Required MongoStore option `db` missing or is not a string.');
|
|
}
|
|
|
|
options.url = 'mongodb://';
|
|
|
|
if (options.username) {
|
|
options.url += options.username + ':' + (options.password || '') + '@';
|
|
}
|
|
|
|
options.url += options.host + ':' + options.port + '/' + options.db;
|
|
|
|
if (options.ssl) options.url += '?ssl=true';
|
|
|
|
if (!options.mongoOptions) {
|
|
options.mongoOptions = {
|
|
server: { auto_reconnect: options.autoReconnect },
|
|
db: { w: options.w }
|
|
};
|
|
}
|
|
}
|
|
|
|
function initWithUrl() {
|
|
MongoClient.connect(options.url, options.mongoOptions || {}, function(err, db) {
|
|
if (!err) self.db = db;
|
|
connectionReady(err);
|
|
});
|
|
}
|
|
|
|
function initWithMongooseConnection() {
|
|
if (options.mongooseConnection.readyState === 1) {
|
|
self.db = options.mongooseConnection.db;
|
|
process.nextTick(connectionReady);
|
|
} else {
|
|
options.mongooseConnection.once('open', function() {
|
|
self.db = options.mongooseConnection.db;
|
|
connectionReady();
|
|
});
|
|
}
|
|
}
|
|
|
|
function initWithNativeDb() {
|
|
self.db = options.db;
|
|
|
|
if (options.db.openCalled || options.db.openCalled === undefined) { // openCalled is undefined in mongodb@2.x
|
|
options.db.collection(options.collection, connectionReady);
|
|
} else {
|
|
options.db.open(connectionReady);
|
|
}
|
|
}
|
|
|
|
this.getCollection = function (done) {
|
|
switch (self.state) {
|
|
case 'connected':
|
|
done(null, self.collection);
|
|
break;
|
|
case 'connecting':
|
|
self.once('connected', function () {
|
|
done(null, self.collection);
|
|
});
|
|
break;
|
|
case 'disconnected':
|
|
done(new Error('Not connected'));
|
|
break;
|
|
}
|
|
};
|
|
|
|
this.getSessionId = function (sid) {
|
|
if (options.hash) {
|
|
return crypto.createHash(options.hash.algorithm).update(options.hash.salt + sid).digest('hex');
|
|
} else {
|
|
return sid;
|
|
}
|
|
};
|
|
|
|
changeState('init');
|
|
|
|
if (options.url) {
|
|
debug('use strategy: `url`');
|
|
initWithUrl();
|
|
} else if (options.mongooseConnection) {
|
|
debug('use strategy: `mongoose_connection`');
|
|
initWithMongooseConnection();
|
|
} else if (options.db && options.db instanceof Db) {
|
|
debug('use strategy: `native_db`');
|
|
process.nextTick(initWithNativeDb);
|
|
} else {
|
|
debug('use strategy: `legacy`');
|
|
buildUrlFromOptions();
|
|
initWithUrl();
|
|
}
|
|
|
|
changeState('connecting');
|
|
|
|
}
|
|
|
|
/**
|
|
* Inherit from `Store`.
|
|
*/
|
|
util.inherits(MongoStore, Store);
|
|
|
|
/**
|
|
* Attempt to fetch session by the given `sid`.
|
|
*
|
|
* @param {String} sid
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
MongoStore.prototype.get = function(sid, callback) {
|
|
if (!callback) callback = _.noop;
|
|
sid = this.getSessionId(sid);
|
|
|
|
var self = this;
|
|
|
|
var query = {
|
|
_id: sid,
|
|
$or: [
|
|
{ expires: { $exists: false } },
|
|
{ expires: { $gt: new Date() } }
|
|
]
|
|
};
|
|
|
|
this.getCollection(function(err, collection) {
|
|
if (err) return callback(err);
|
|
collection.findOne(query, function(err, session) {
|
|
if (err) {
|
|
debug('not able to execute `find` query for session: ' + sid);
|
|
return callback(err);
|
|
}
|
|
|
|
if (session) {
|
|
var s;
|
|
try {
|
|
s = self.options.unserialize(session.session);
|
|
if(self.options.touchAfter > 0 && session.lastModified){
|
|
s.lastModified = session.lastModified;
|
|
}
|
|
} catch (err) {
|
|
debug('unable to deserialize session');
|
|
callback(err);
|
|
}
|
|
callback(null, s);
|
|
} else {
|
|
callback();
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Commit the given `sess` object associated with the given `sid`.
|
|
*
|
|
* @param {String} sid
|
|
* @param {Session} sess
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
MongoStore.prototype.set = function(sid, session, callback) {
|
|
if (!callback) callback = _.noop;
|
|
sid = this.getSessionId(sid);
|
|
|
|
// removing the lastModified prop from the session object before update
|
|
if(this.options.touchAfter > 0 && session && session.lastModified){
|
|
delete session.lastModified;
|
|
}
|
|
|
|
var s;
|
|
|
|
try {
|
|
s = {_id: sid, session: this.options.serialize(session)};
|
|
} catch (err) {
|
|
debug('unable to serialize session');
|
|
callback(err);
|
|
}
|
|
|
|
if (session && session.cookie && session.cookie.expires) {
|
|
s.expires = new Date(session.cookie.expires);
|
|
} else {
|
|
// If there's no expiration date specified, it is
|
|
// browser-session cookie or there is no cookie at all,
|
|
// as per the connect docs.
|
|
//
|
|
// So we set the expiration to two-weeks from now
|
|
// - as is common practice in the industry (e.g Django) -
|
|
// or the default specified in the options.
|
|
s.expires = new Date(Date.now() + this.options.ttl * 1000);
|
|
}
|
|
|
|
if(this.options.touchAfter > 0){
|
|
s.lastModified = new Date();
|
|
}
|
|
|
|
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);
|
|
callback(err);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Touch the given `sess` object associated with the given `sid`.
|
|
*
|
|
* @param {String} sid
|
|
* @param {Session} session
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
MongoStore.prototype.touch = function (sid, session, callback) {
|
|
|
|
var updateFields = {},
|
|
touchAfter = this.options.touchAfter * 1000,
|
|
lastModified = session.lastModified ? session.lastModified.getTime() : 0,
|
|
currentDate = new Date();
|
|
|
|
sid = this.getSessionId(sid);
|
|
|
|
callback = callback ? callback : _.noop;
|
|
|
|
// if the given options has a touchAfter property, check if the
|
|
// current timestamp - lastModified timestamp is bigger than
|
|
// the specified, if it's not, don't touch the session
|
|
if(touchAfter > 0 && lastModified > 0){
|
|
|
|
var timeElapsed = currentDate.getTime() - session.lastModified;
|
|
|
|
if(timeElapsed < touchAfter){
|
|
return callback();
|
|
} else {
|
|
updateFields.lastModified = currentDate;
|
|
}
|
|
|
|
}
|
|
|
|
if (session && session.cookie && session.cookie.expires) {
|
|
updateFields.expires = new Date(session.cookie.expires);
|
|
} else {
|
|
updateFields.expires = new Date(Date.now() + this.options.ttl * 1000);
|
|
}
|
|
|
|
this.getCollection(function(err, collection) {
|
|
if (err) return callback(err);
|
|
collection.update({ _id: sid }, { $set: updateFields }, { safe: true }, function (err, result) {
|
|
if (err) {
|
|
debug('not able to touch session: %s (error)', sid);
|
|
callback(err);
|
|
} else if (result.nModified === 0) {
|
|
debug('not able to touch session: %s (not found)', sid);
|
|
callback(new Error('Unable to find the session to touch'));
|
|
}
|
|
callback();
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Destroy the session associated with the given `sid`.
|
|
*
|
|
* @param {String} sid
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
MongoStore.prototype.destroy = function(sid, callback) {
|
|
if (!callback) callback = _.noop;
|
|
sid = this.getSessionId(sid);
|
|
|
|
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);
|
|
callback(err);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Fetch number of sessions.
|
|
*
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
MongoStore.prototype.length = function(callback) {
|
|
if (!callback) callback = _.noop;
|
|
this.getCollection(function(err, collection) {
|
|
if (err) return callback(err);
|
|
collection.count({}, function(err, count) {
|
|
if (err) debug('not able to count sessions');
|
|
callback(err, count);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Clear all sessions.
|
|
*
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
MongoStore.prototype.clear = function(callback) {
|
|
if (!callback) callback = _.noop;
|
|
this.getCollection(function(err, collection) {
|
|
if (err) return callback(err);
|
|
collection.drop(function(err) {
|
|
if (err) debug('not able to clear sessions');
|
|
callback(err);
|
|
});
|
|
});
|
|
};
|
|
|
|
return MongoStore;
|
|
};
|