/**
* @class node_modules.restify_session
*
* @author Marcello Gesmundo
*
* This module manage session for a restify server without cookies. It uses Redis (>=2.0.0) as fast session store.
* Derived from redis-session.
*
* # Example
*
* var restify = require('restify'),
* session = require('restify-session')({
* debug : true,
* ttl : 2
* });
* var server = restify.createServer();
*
* // attach session manager
* server.use(session.sessionManager)
*
* // attach a route
* server.get('/', function(req, res, next){
* res.send({ success: true, session: req.session });
* return next();
* });
*
* // start the server
* server.listen(3000);
*
* // Save this file as server.js and start it in a terminal window:
*
* $ node server.js
*
* // Open your browser and put the address of your server:
*
* http://localhost:3000
*
* // Now you see an answer like this:
*
* {"success":true,"session":{"sid":"ViS5pHE5n8McblTATbyFUJTGJyzVFeXOcAEZ41Zs"}}
*
* // You can see your session id (sid) into the response header:
*
* Session-Id ViS5pHE5n8McblTATbyFUJTGJyzVFeXOcAEZ41Zs
*
* For more information see test/test.js into package folder.
*
*
*
* # License
*
* Copyright (c) 2012 Mitchell Simoens (https://github.com/mitchellsimoens/redis-session)
*
* Copyright (c) 2012-2014 Yoovant by Marcello Gesmundo. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of Yoovant nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
module.exports = function(config) {
// namespace
var session = {};
var redis = require('redis');
var utils = require('object_utils');
var Random = require('secure-rnd');
var rnd = new Random();
/**
* @ignore
* @private
*/
session.name = 'session';
/**
* @ignore
* @private
*/
session.merge = utils.merge;
config = config || {};
/**
* Configuration
*/
session.config = {
/**
* @cfg {Integer} ttl=600 The time to live for the session in seconds
*/
ttl: 600, // 10min
/**
* @cfg {Boolean} debug=true true to enable debug session
*/
debug: false,
/**
* @cfg {Object} logger=console The logger. If you use winston set your desired log level and set debug = true.
*/
logger: console,
/**
* @cfg {Integer} sidLength=40 The number of characters to create the session ID.
*/
sidLength: 40,
/**
* @cfg {Boolean} persist=false Persistence of session
* If persist is false, the session will expire after the ttl config.
* If persist is true, the session will never expire and ttl config will be ignored.
*/
persist: false,
/**
* @cfg {connection} connection REDIS connection information
* @cfg {Number} connection.port=6379 REDIS port
* @cfg {String} connection.host='127.0.0.1' REDIS host
* @cfg {String} [connection.db] REDIS database
* @cfg {String} [connection.user] REDIS user
* @cfg {String} [connection.pass] REDIS password
*/
connection: {
port: 6379,
host: '127.0.0.1',
db : undefined,
user: undefined,
pass: undefined
},
* @cfg {String} sidHeader='Session-Id' The Header section name to store the session identifier
*/
sidHeader: 'Session-Id'
};
// merge new config with default config
session.merge(session.config, config);
if ('function' !== typeof session.config.logger.debug) {
session.config.logger.debug = session.config.logger.log;
}
if ('function' !== typeof session.config.logger.error) {
session.config.logger.error = session.config.logger.log;
}
if ('function' !== typeof session.config.logger.info) {
session.config.logger.info = session.config.logger.log;
}
var cfg = session.config,
con = cfg.connection;
// create redis client
session.client = new redis.createClient(con.port, con.host, con);
var isNull = utils.isNull;
if (con.pass && con.pass.length > 0) {
session.client.auth(con.pass, function(err) {
if (err) {
throw err;
}
});
}
if (con.db) {
session.client.select(con.db);
session.client.on('connect', function() {
session.client.send_anyways = true;
session.client.select(con.db);
session.client.send_anyways = false;
});
}
// handle events
session.client.on('error', function (err) {
cfg.logger.error(session.name + ': database ERROR');
cfg.logger.error(err);
});
session.client.on('connect', function () {
cfg.logger.info(session.name + ': connected to Redis database');
});
/**
* Create session identifier
*
* @param {Function} callback Function called when the identifier is created
* @return {callback(err, sid)} The callback to execute as result
* @param {String} callback.err Error if occurred
* @param {String} callback.sid session identifier
* @private
*/
session.createSid = function(callback) {
var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz',
codes = [],
cLen = chars.length,
len = cfg.sidLength,
sid = '',
i = 0,
rNum;
codes.length = cLen;
// generate secure randoms
rnd(codes);
for ( i = 0; i < len; i++) {
rNum = Math.floor(codes[i] * cLen / 255);
sid += chars.substring(rNum, rNum + 1);
}
session.client.exists(sid, function(err, exists) {
if (err && cfg.debug) {
cfg.logger.error(session.name + ': sid creation ERROR');
cfg.logger.error(err);
}
if (exists === 1) {
session.createSid(callback);
} else {
if (cfg.debug) {
cfg.logger.debug(session.name + ': sid created ' + sid);
}
callback.call(session, err, sid);
}
});
};
/**
* Save session data
*
* @param {String} sid session identifier
* @param {Object} data session data
* @param {Function} callback Function called when the session is saved
* @return {callback(err, status)} The callback to execute as result
* @param {String} callback.err Error if occurred
* @param {String} callback.status Result from Redis query
*/
session.save = function(sid, data, callback) {
if (isNull(sid)) {
if (callback) {
callback.call(session, 'no sid given', undefined);
}
return;
}
var ttl = session.config.ttl,
info = JSON.stringify(data),
setCallback = function(err, status) {
if (err && cfg.debug) {
cfg.logger.error(session.name + ': saving sid ' + sid + ' ERROR');
cfg.logger.error(err);
}
if (callback) {
callback.call(session, err, status);
}
};
if (cfg.persist) {
session.client.set(sid, info, setCallback);
} else {
session.client.setex(sid, ttl, info, setCallback);
}
};
/**
* Load session data
*
* @param {String} sid session identifier
* @param {Function} callback Function called when the session is loaded
* @return {callback(err, data)} The callback to execute as result
* @param {String} callback.err Error if occurred
* @param {Object} callback.data session data
*/
session.load = function(sid, callback) {
if (isNull(sid)) {
if (callback) {
callback.call(session, 'no sid given', undefined);
}
return;
}
session.client.get(sid, function(err, info) {
var data;
if (err) {
if (cfg.debug) {
cfg.logger.debug(session.name + ': sid ' + sid + ' loading ERROR');
cfg.logger.debug(err);
}
} else if (!isNull(info)) {
data = JSON.parse(info.toString());
}
if (callback) {
callback.call(session, err, data);
}
});
};
/**
* Update the ttl for the session
*
* @param {String} sid session identifier
* @param {Function} callback Function called when the session is refreshed
* @return {callback(err, active)} The callback to execute as result
* @param {String} callback.err Error if occurred
* @param {Boolean} callback.active True if the sid is active (not expired)
*/
session.refresh = function(sid, callback) {
if (isNull(sid)) {
if (callback) {
callback.call(session, 'no sid given', false);
}
return;
}
if (cfg.persist) {
callback.call(session, undefined, true);
return;
}
session.client.expire(sid, session.config.ttl, function(err, status){
if (err && cfg.debug) {
cfg.logger.debug(session.name + ': sid ' + sid + ' refreshing ERROR');
cfg.logger.debug(err);
}
if (callback) {
callback.call(session, err, (status === 1));
}
});
};
/**
* Check if a session identifier exists
*
* @param {String} sid session identifier
* @param {Function} callback Function called when the session identifier is verified
* @return {callback(err, exists)} The callback to execute as result
* @param {String} callback.err Error if occurred
* @param {Boolean} callback.exists Return true if the session exists
*/
session.exists = function(sid, callback) {
if (isNull(sid)) {
callback.call(session, undefined, false);
return;
}
session.client.exists(sid, function(err, status) {
if (err && cfg.debug) {
cfg.logger.debug(session.name + ': sid ' + sid + ' does not exist ERROR');
cfg.logger.debug(err);
}
callback.call(session, err, (status === 1) );
});
};
/**
* Get all session identifier
*
* @param {Function} callback Function called when the session identifiers are loaded
* @return {callback(err, keys)} The callback to execute as result
* @param {String} callback.err Error if occurred
* @param {Array} callback.keys All valid session identifiers
* @private
*/
session.getAllKeys = function(callback) {
if (!cfg.debug) {
cfg.logger.debug(session.name + ': unable to get all session identifiers when not in debug mode');
if (callback) {
callback.call(session, 'unable to get all session identifiers when not in debug mode');
}
return;
}
session.client.keys('*', function(err, keys) {
if (callback) {
callback.call(session, err, keys);
}
});
};
/**
* Destroy a session
*
* @param {String} sid session identifier
* @param {Function} callback Function called when finishing
* @return {callback(err, status)} The callback to execute as result
* @param {String} callback.err Returned error if occurred
* @param {String} callback.status Returned status code from Redis
* @private
*/
session.destroy = function(sid, callback) {
if (isNull(sid)) {
if (callback) {
callback.call(session, 'no sid given', 0);
}
return;
}
session.client.del(sid, function(status) {
if (callback) {
callback.call(session, undefined, status);
}
});
};
/**
* Destroy all sessions
*
* @param {Function} callback Function called when finishing
* @return {callback(status)} The callback to execute as result
* @param {String} callback.err Returned error if occurred
* @param {String} callback.status Returned status code from Redis
* @private
*/
session.destroyAll = function(callback) {
if (!cfg.debug) {
cfg.logger.debug(session.name + ': unable to destroy all sessions when not in debug mode');
if (callback) {
callback.call(session, 'unable to destroy all sessions when not in debug mode');
}
return;
}
session.client.flushall(function(status) {
if (callback) {
callback.call(session, undefined, status);
}
});
};
/**
* Set data in session object to use in other middlewares below this
*
* @param {String} sid The session identifier
* @param {{sid: String, ...}} data The data to store in session
* @param {{session: data, ...}} req Client request with new __session__ property
* containing session data
* @param {Object} res Server response
* @param {Function} next Calback to execute at the end of saving
*/
session.setSessionData = function(sid, data, req, res, next) {
if (isNull(sid)) {
next();
return;
}
if (isNull(data)) {
data = {};
}
data.sid = sid;
session.save(sid, data, function(err, status){
if (!err) {
// set session in request to use it in other middlewares
// attached below this
req.session = data;
res.setHeader(cfg.sidHeader, sid);
}
next();
});
};
/**
* Closes the connection to the redis server
*
* @param {Function} callback Callback to execute once connection is closed
*/
session.disconnect = function (callback) {
session.client.quit();
session.client.on('end', callback);
};
/**
* Manage session in http requests
*
* @param {Object} req Request from client
* @param {Object} res Response from server
* @param {Function} next Next function to execute in server
*/
session.sessionManager = function (req, res, next) {
if (cfg.debug) {
cfg.logger.debug(session.name + ': request url: ' + req.url);
}
var reqSid = req.headers[session.config.sidHeader.toLowerCase()];
session.exists(reqSid, function(existErr, active){
if (existErr) {
next();
} else if (active) {
// load the session
session.load(reqSid, function(loadErr, data){
if (!loadErr) {
session.setSessionData(reqSid, data, req, res, next);
} else {
next();
}
});
} else {
session.createSid(function(createErr, sid) {
if (!createErr) {
session.setSessionData(sid, {}, req, res, next);
} else {
next();
}
});
}
});
};
return session;
};