UNPKG

@balderdash/sails-edge

Version:

API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)

295 lines (243 loc) 10 kB
/** * Module dependencies. */ var path = require('path'); var util = require('util'); var _ = require('lodash'); // generateSecret is used to generate a one-off session secret if one wasn't configured var generateSecret = require('./generateSecret'); // (this dependency is just for creating new cookies) var uid = require('uid-safe').sync; // (these two dependencies are only here for sails.session.parseSessionIdFromCookie(), // which is only here to enable socket lifecycle callbacks) var parseCookie = require('cookie').parse; var stringifyCookie = require('cookie').serialize; var unsignCookie = require('cookie-signature').unsign; var signCookie = require('cookie-signature').sign; module.exports = function(app) { var getSession = function(sessionId, errorMessage, cb) { app.config.session.store.get(sessionId, function (err, session) { if (err) return cb(err); if (!session) { return cb((function _createError(){ var e = new Error(errorMessage); e.code = 'E_SESSION'; return e; })()); } return cb(null, session); }); }; // `session` hook definition var SessionHook = { defaults: { session: { adapter: 'memory', key: 'sails.sid' } }, /** * Normalize and validate configuration for this hook. * Then fold any modifications back into `sails.config` */ configure: function() { // Validate config // Ensure that secret is specified if a custom session store is used if (app.config.session) { if (!_.isObject(app.config.session)) { throw new Error('Invalid custom session store configuration!\n' + '\n' + 'Basic usage ::\n' + '{ session: { adapter: "memory", secret: "someVerySecureString", /* ...if applicable: host, port, etc... */ } }' + '\n\nCustom usage ::\n' + '{ session: { store: { /* some custom connect session store instance */ }, secret: "someVerySecureString", /* ...custom settings.... */ } }' ); } } // If session config is set, but secret is undefined, set a secure, one-time use secret if (!app.config.session || !app.config.session.secret) { app.log.verbose('Session secret not defined-- automatically generating one for now...'); if (app.config.environment === 'production') { app.log.warn('Session secret should be manually specified in production!'); app.log.warn('Automatically generating one for now...'); app.log.error('This generated session secret is NOT OK for production!'); app.log.error('It will change each time the server starts and break multi-instance deployments.'); app.log.blank(); app.log.error('To set up a session secret, add or update it in `config/session.js`:'); app.log.error('module.exports.session = { secret: "keyboardcat" }'); app.log.blank(); } app.config.session.secret = generateSecret(); } // Backwards-compatibility / shorthand notation // (allow mongo or redis session stores to be specified directly) if (app.config.session.adapter === 'redis') { app.config.session.adapter = 'connect-redis'; } else if (app.config.session.adapter === 'mongo') { app.config.session.adapter = 'connect-mongo'; } }, /** * Create a connection to the configured session store * and keep it around * * @api private */ initialize: function(cb) { var sessionConfig = app.config.session; // console.log('Initializing session hook...'); // Intepret session adapter config and "new up" a session store if (_.isObject(sessionConfig) && !_.isObject(sessionConfig.store)) { // Unless the session is explicitly disabled, require the appropriate adapter if (sessionConfig.adapter) { // 'memory' is a special case if (sessionConfig.adapter === 'memory') { var MemoryStore = require('express').session.MemoryStore; sessionConfig.store = new MemoryStore(); } // Try and load the specified adapter from the local sails project, // or catch and return error: else { var COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR = function (adapter, packagejson, e) { var errMsg; if (e && typeof e === 'object' && e instanceof Error) { errMsg = e.stack; } else { errMsg = util.inspect(e); } var output = 'Could not load Connect session adapter :: ' + adapter + '\n'; if (packagejson && !packagejson.main) { output+='(If this is your module, make sure that the module has a "main" configuration in its package.json file)'; } output+='\nError from adapter:\n'+ errMsg+'\n\n'; // Recommend installation of the session adapter: output += 'Do you have the Connect session adapter installed in this project?\n'; output += 'Try running the following command in your project\'s root directory:\n'; var installRecommendation = 'npm install '; if (adapter === 'connect-redis') { installRecommendation += 'connect-redis@1.4.5'; installRecommendation += '\n(Note that `connect-redis@1.5.0` introduced breaking changes- make sure you have v1.4.5 installed!)'; } else { installRecommendation += adapter; installRecommendation +='\n(Note: Make sure the version of the Connect adapter you install is compatible with Express 3/Sails v0.10)'; } installRecommendation += '\n'; output += installRecommendation; return output; }; try { // Determine the path to the adapter by using the "main" described in its package.json file: var pathToAdapterDependency; var pathToAdapterPackage = path.resolve(app.config.appPath, 'node_modules', sessionConfig.adapter ,'package.json'); var adapterPackage; try { adapterPackage = require(pathToAdapterPackage); pathToAdapterDependency = path.resolve(app.config.appPath, 'node_modules', sessionConfig.adapter, adapterPackage.main); } catch (e) { return cb(COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR(sessionConfig.adapter, adapterPackage, e)); } var SessionAdapter = require(pathToAdapterDependency); var CustomStore = SessionAdapter(require('express')); sessionConfig.store = new CustomStore(sessionConfig); } catch (e) { // TODO: negotiate error and give better error msg depending on code return cb(COULD_NOT_REQUIRE_CONNECT_ADAPTER_ERR(sessionConfig.adapter, adapterPackage, e)); } } } } // Expose hook as `sails.session` app.session = SessionHook; return cb(); }, /** * Generate a cookie to represent a new session. * * @return {String} * @api private */ generateNewSidCookie: function (){ var sid = uid(24); var signedSid = 's:' + signCookie(sid, app.config.session.secret); var cookie = stringifyCookie(app.config.session.key, signedSid, {}); return cookie; }, /** * Parse and unsign (i.e. decrypt) the provided cookie to get the session id. * * (adapted from code in the `express-session`) * (TODO: pull out into separate module as part of jshttp/pillarjs) * * @param {String} cookie * @return {String} [sessionId] * * @throws {Error} If cookie cannot be parsed or unsigned */ parseSessionIdFromCookie: function (cookie){ // e.g. "lolcatparty" var sessionSecret = app.config.session.secret; // Parse cookie var parsedSidCookie = parseCookie(cookie)[app.config.session.key]; if (typeof parsedSidCookie !== 'string') { throw (function createError(){ var err = new Error('No sid cookie exists'); err.status = 401; err.code = 'E_SESSION_PARSE_COOKIE'; return err; })(); } if (parsedSidCookie.substr(0, 2) !== 's:') { throw (function createError(){ var err = new Error('Cookie unsigned'); err.status = 401; err.code = 'E_SESSION_PARSE_COOKIE'; return err; })(); } // Unsign cookie var sessionId = unsignCookie(parsedSidCookie.slice(2), sessionSecret); if (sessionId === false) { throw (function createError(){ var err = new Error('Cookie signature invalid'); err.status = 401; err.code = 'E_SESSION_PARSE_COOKIE'; return err; })(); } return sessionId; }, /** * @param {String} sessionId * @param {Function} cb * * @api private */ get: function(sessionId, cb) { if (!_.isFunction(cb)) { throw new Error('Invalid usage :: `sails.hooks.session.get(sessionId, cb)`'); } return getSession(sessionId, 'Session could not be loaded', cb); }, /** * @param {String} sessionId * @param {} data * @param {Function} cb * * @api private */ set: function(sessionId, data, cb) { if (!_.isFunction(cb)) { throw new Error('Invalid usage :: `sails.hooks.session.set(sessionId, data, cb)`'); } return app.config.session.store.set(sessionId, data, function (err) { if (err) return cb(err); return getSession(sessionId, 'Session could not be saved', cb); }); } }; return SessionHook; };