diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..edd6aa7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +lib/* linguist-generated diff --git a/elephas.js b/elephas.js index 70ecd9a..7d11991 100644 --- a/elephas.js +++ b/elephas.js @@ -1,2 +1 @@ -require('babel-core/register'); -module.exports = require('./lib/framework'); \ No newline at end of file +module.exports = require('./lib/framework'); diff --git a/lib/Service.js b/lib/Service.js index a8d8df1..fc8bd8d 100644 --- a/lib/Service.js +++ b/lib/Service.js @@ -4,34 +4,31 @@ var util = require("util"), events = require("events"); function Service() { - events.EventEmitter.call(this); + events.EventEmitter.call(this); } - util.inherits(Service, events.EventEmitter); -// for connection retry. +// for connection retry. Service.prototype.retryCount = 0; // max retry set as 3. Service.prototype.MAX_RETRY = 3; - -Service.prototype.connectionFailed = function() { +Service.prototype.connectionFailed = function () { this.retryCount++; }; -Service.prototype.isRetry = function() { +Service.prototype.isRetry = function () { return this.retryCount < this.MAX_RETRY; }; - -Service.prototype.serviceConnected = function() { +Service.prototype.serviceConnected = function () { this.retryCount = 0; this.emit('connected'); }; -Service.prototype.serviceDown = function() { +Service.prototype.serviceDown = function () { this.emit('serviceDown'); }; diff --git a/lib/__test__/client-test.js b/lib/__test__/client-test.js index 8622ae6..bb03f03 100644 --- a/lib/__test__/client-test.js +++ b/lib/__test__/client-test.js @@ -1,32 +1,42 @@ -import {expect} from 'chai'; -import sinon from 'sinon'; -import client from '../client'; +'use strict'; -const payload = 'test'; +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } -global.Primus = function() { +var _chai = require('chai'); + +var _sinon = require('sinon'); + +var _sinon2 = _interopRequireDefault(_sinon); + +var _client = require('../client'); + +var _client2 = _interopRequireDefault(_client); + +var payload = 'test'; + +global.Primus = function () { return { - on(e, cb) { + on: function on(e, cb) { return cb('payload'); }, - write: sinon.spy() + write: _sinon2['default'].spy() }; }; -const myStore = { - dispatch: sinon.spy() +var myStore = { + dispatch: _sinon2['default'].spy() }; -describe('client', () => { - it('subscribe to redux store', () => { - client.connect(myStore); +describe('client', function () { + it('subscribe to redux store', function () { + _client2['default'].connect(myStore); - expect(myStore.dispatch.called).to.equal(true); + (0, _chai.expect)(myStore.dispatch.called).to.equal(true); }); - it('should dispatch to server and client', () => { - client.dispatch('actionTest'); + it('should dispatch to server and client', function () { + _client2['default'].dispatch('actionTest'); - expect(myStore.dispatch.calledWith('actionTest')).to.equal(true); + (0, _chai.expect)(myStore.dispatch.calledWith('actionTest')).to.equal(true); }); -}); +}); \ No newline at end of file diff --git a/lib/__test__/clientSocket-test.js b/lib/__test__/clientSocket-test.js index c180022..5cf24e7 100644 --- a/lib/__test__/clientSocket-test.js +++ b/lib/__test__/clientSocket-test.js @@ -1,17 +1,27 @@ -import {expect} from 'chai'; -import sinon from 'sinon'; -import clientSocket from '../clientSocket'; - -describe('clientSocket', () => { - it('should dispatch to the client', () => { - const spark = { - write: sinon.spy() +'use strict'; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _chai = require('chai'); + +var _sinon = require('sinon'); + +var _sinon2 = _interopRequireDefault(_sinon); + +var _clientSocket = require('../clientSocket'); + +var _clientSocket2 = _interopRequireDefault(_clientSocket); + +describe('clientSocket', function () { + it('should dispatch to the client', function () { + var spark = { + write: _sinon2['default'].spy() }; - const client = clientSocket(spark); + var client = (0, _clientSocket2['default'])(spark); client.dispatch('myAction'); - expect(spark.write.calledWith('myAction')).to.equal(true); + (0, _chai.expect)(spark.write.calledWith('myAction')).to.equal(true); }); -}); +}); \ No newline at end of file diff --git a/lib/__test__/wsRouter-test.js b/lib/__test__/wsRouter-test.js index 03a7f81..ea1896d 100644 --- a/lib/__test__/wsRouter-test.js +++ b/lib/__test__/wsRouter-test.js @@ -1,37 +1,44 @@ -import {expect} from 'chai'; -import sinon from 'sinon'; -import wsRouter from '../wsRouter'; +'use strict'; -const _testRoutes = [ - { MY_TEST_ACTION: sinon.spy() }, - { ANOTHER_ACTION: ()=>{ +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _chai = require('chai'); + +var _sinon = require('sinon'); + +var _sinon2 = _interopRequireDefault(_sinon); + +var _wsRouter = require('../wsRouter'); + +var _wsRouter2 = _interopRequireDefault(_wsRouter); + +var _testRoutes = [{ MY_TEST_ACTION: _sinon2['default'].spy() }, { ANOTHER_ACTION: function ANOTHER_ACTION() { var e = new Error('dummy'); console.log(e.stack); - } - - } -]; - -describe('wsRouter', () => { - it('has no routes by default', () => { - expect(wsRouter.hasRoutes()).to.equal(false); + } + +}]; + +describe('wsRouter', function () { + it('has no routes by default', function () { + (0, _chai.expect)(_wsRouter2['default'].hasRoutes()).to.equal(false); }); - it('can add websocket routes/action handlers', () => { - wsRouter.setActionHandlers(_testRoutes); + it('can add websocket routes/action handlers', function () { + _wsRouter2['default'].setActionHandlers(_testRoutes); - expect(wsRouter.hasRoutes()).to.equal(true); + (0, _chai.expect)(_wsRouter2['default'].hasRoutes()).to.equal(true); }); - it('can route to the correct action', () => { - const client = {}; - const action = {type: 'MY_TEST_ACTION', payload: 'test'}; + it('can route to the correct action', function () { + var client = {}; + var action = { type: 'MY_TEST_ACTION', payload: 'test' }; - wsRouter.route(client, action); + _wsRouter2['default'].route(client, action); - const fn = _testRoutes[0].MY_TEST_ACTION; + var fn = _testRoutes[0].MY_TEST_ACTION; - expect(fn.called).to.equal(true); + (0, _chai.expect)(fn.called).to.equal(true); }); // it('should not execute an action handler if there is no match', () => { @@ -43,4 +50,4 @@ describe('wsRouter', () => { // expect(_testRoutes[1].ANOTHER_ACTION.called).to.equal(false); // }); -}); +}); \ No newline at end of file diff --git a/lib/client.js b/lib/client.js index 9a1f7f0..90d8602 100644 --- a/lib/client.js +++ b/lib/client.js @@ -1,25 +1,27 @@ +'use strict'; + var _primus; var _store; var elephas = { - connect: function(store) { + connect: function connect(store) { if (!_primus) { _primus = new Primus(); _store = store; - _primus.on('data', function(action) { + _primus.on('data', function (action) { store.dispatch(action); }); } }, - dispatch: function(action) { + dispatch: function dispatch(action) { _store.dispatch(action); _primus.write(action); }, - subscribe(cb) { + subscribe: function subscribe(cb) { _primus.on('data', cb); } }; -module.exports = elephas; +module.exports = elephas; \ No newline at end of file diff --git a/lib/clientSocket.js b/lib/clientSocket.js index 33f811e..d53d02a 100644 --- a/lib/clientSocket.js +++ b/lib/clientSocket.js @@ -1,27 +1,25 @@ +'use strict'; + var logger = require('./logger'); var chalk = require('chalk'); function log(client, action) { - const msg = [ - chalk.gray('DISPATCH'), - action.type, - chalk.red('--unkown--' + client.id) - ].join(' '); + var msg = [chalk.gray('DISPATCH'), action.type, chalk.red('--unkown--' + client.id)].join(' '); logger.info(msg); } var clientSocket = function clientSocket(spark) { return { - id: spark.id, - on: function(e, cb) { - spark.on(e, cb); - }, - dispatch: function(action) { - spark.write(action); - log(this, action); - } + id: spark.id, + on: function on(e, cb) { + spark.on(e, cb); + }, + dispatch: function dispatch(action) { + spark.write(action); + log(this, action); + } }; }; -module.exports = clientSocket; +module.exports = clientSocket; \ No newline at end of file diff --git a/lib/express-logger.js b/lib/express-logger.js index 708deed..23ab13b 100644 --- a/lib/express-logger.js +++ b/lib/express-logger.js @@ -1,3 +1,5 @@ +'use strict'; + var logger = require('./logger'); var chalk = require('chalk'); var statsd = require('./statsd'); @@ -6,8 +8,7 @@ var cluster = require('cluster'); function _calcLogLevel(statusCode) { if (statusCode >= 500) { return 'error'; - } - else if (statusCode >= 400) { + } else if (statusCode >= 400) { return 'warning'; } @@ -19,11 +20,9 @@ function _formatStatus(statusCode) { if (statusCode >= 500) { color = 'red'; - } - else if (statusCode >= 400) { + } else if (statusCode >= 400) { color = 'yellow'; - } - else if (statusCode >= 300) { + } else if (statusCode >= 300) { color = 'cyan'; } @@ -35,11 +34,9 @@ function _formatResponseTime(ms) { if (ms >= 500) { color = 'red'; - } - else if (ms >= 300) { + } else if (ms >= 300) { color = 'magenta'; - } - else if (ms >= 100) { + } else if (ms >= 100) { color = 'yellow'; } @@ -59,40 +56,32 @@ function _log(req, res, responseTime) { if (req && req.user && req.user.username) { user = chalk.gray(req.user.username); - } - else if (req && req.session && req.session.passport && req.session.passport.user) { + } else if (req && req.session && req.session.passport && req.session.passport.user) { user = chalk.gray(req.session.passport.user); - } - else { + } else { user = chalk.red('anonymous'); } - var msg = [ - chalk.gray(req.method), - _formatStatus(res.statusCode), - _formatResponseTime(responseTime), - req.originalUrl, - user, - // chalk.gray('pid=' + pid) - ].join(' '); + var msg = [chalk.gray(req.method), _formatStatus(res.statusCode), _formatResponseTime(responseTime), req.originalUrl, user]. + // chalk.gray('pid=' + pid) + join(' '); logger[level](msg, _heapDump); statsd.timing('api.response_time', responseTime); } -process.on('uncaughtException', function(err) { +process.on('uncaughtException', function (err) { logger.error(err.stack); }); - -module.exports.logger = function(req, res, next) { +module.exports.logger = function (req, res, next) { var _start = new Date(); // I feel really disgusted doing this. Can't waith to switch to Koa... var end = res.end; - res.end = function(chunk, encoding) { - var responseTime = (new Date() - _start); + res.end = function (chunk, encoding) { + var responseTime = new Date() - _start; res.end = end; res.end(chunk, encoding); @@ -103,13 +92,13 @@ module.exports.logger = function(req, res, next) { next(); }; -module.exports.errorLogger = function(err, req, res, next) { +module.exports.errorLogger = function (err, req, res, next) { var _start = new Date(); // I feel really disgusted doing this. Can't waith to switch to Koa... var end = res.end; - res.end = function(chunk, encoding) { - var responseTime = (new Date() - _start); + res.end = function (chunk, encoding) { + var responseTime = new Date() - _start; res.end = end; res.end(chunk, encoding); @@ -118,4 +107,4 @@ module.exports.errorLogger = function(err, req, res, next) { }; next(); -}; +}; \ No newline at end of file diff --git a/lib/findFiles.js b/lib/findFiles.js index 4666c2c..0ee94af 100644 --- a/lib/findFiles.js +++ b/lib/findFiles.js @@ -3,10 +3,12 @@ var recursive = require('recursive-readdir'); module.exports = function findFiles(path, suffix, cb) { - recursive(path, function(err, files) { - if (err) { return cb ? cb(err) : null; } - - var routers = files.filter(function(f) { + recursive(path, function (err, files) { + if (err) { + return cb ? cb(err) : null; + } + + var routers = files.filter(function (f) { var len = suffix.length; return f.substr(f.length - len, len) === suffix; }); diff --git a/lib/framework.js b/lib/framework.js index b37f995..3b0fd08 100644 --- a/lib/framework.js +++ b/lib/framework.js @@ -8,7 +8,6 @@ var express = require('express'), var _statsd = require('./statsd'); - var services = require('./services'), middleware = require('./middleware'), routes = require('./routes'), @@ -22,7 +21,8 @@ var DEFAULT_OPTIONS = { ping: true, server: { port: 3000, - cluster: false + cluster: false, + listen: true }, logger: { level: 'debug' @@ -36,7 +36,7 @@ function _addStatsD(options, app) { new StatsD(options); - app.use('/api', function(req, res, next) { + app.use('/api', function (req, res, next) { statsd_client.increment('api.request'); next(); }); @@ -47,13 +47,12 @@ function _addStatsD(options, app) { function _HttpsOnly(options, app) { if (options) { - app.use(function(req, res, next) { + app.use(function (req, res, next) { var proto = req.headers['x-forwarded-proto'] || req.protocol; if (!proto || proto === 'https') { next(); - } - else { + } else { return res.redirect('https://' + req.headers.host + req.url); } }); @@ -64,7 +63,7 @@ function _HttpsOnly(options, app) { function _ping(options, app) { if (options) { - app.use('/ping', function(req, res) { + app.use('/ping', function (req, res) { res.end('200 Service Available'); }); } @@ -72,18 +71,15 @@ function _ping(options, app) { function elephas(options) { var app = express(); - options = _.defaults(options || {}, DEFAULT_OPTIONS); logger.transports.console.level = options.logger.level || 'debug'; - if (options.__dirname) { options.routes_root_path = options.routes_root_path || options.__dirname; options.services_root_path = options.services_root_path || options.__dirname; options.static_root_path = options.static_root_path || options.__dirname + '/public'; - } - else { + } else { logger.error('Require option `__dirname` in elephas config'); process.exit(1); } @@ -95,62 +91,83 @@ function elephas(options) { _HttpsOnly(options.httpsOnly, app); function _toTask(fn) { - return function(done) { + return function (done) { fn(done, app); }; } return { - createServer: function(hooks) { + app: app, + createServer: function createServer(hooks) { hooks = hooks || {}; var timerResults = []; - var timerTask = function(taskName) { - return function(done) { - timerResults.push({task: taskName, completed: new Date()}); + var timerTask = function timerTask(taskName) { + return function (done) { + timerResults.push({ task: taskName, completed: new Date() }); return done(); }; }; - var tasks = []; // SERVICES - if (hooks.beforeServices) { tasks.push(_toTask(hooks.beforeServices)); } + if (hooks.beforeServices) { + tasks.push(_toTask(hooks.beforeServices)); + } tasks.push(timerTask('beforeServices')); - tasks.push(function(done) { services(done, options.services_root_path); }); + tasks.push(function (done) { + services(done, options.services_root_path); + }); tasks.push(timerTask('services')); // MIDDLEWARE - if (hooks.beforeMiddleware) { tasks.push(_toTask(hooks.beforeMiddleware)); } + if (hooks.beforeMiddleware) { + tasks.push(_toTask(hooks.beforeMiddleware)); + } tasks.push(timerTask('beforeMiddleware')); - tasks.push(function(done) { middleware(done, app, options, options.redisClient); }); + tasks.push(function (done) { + middleware(done, app, options, options.redisClient); + }); tasks.push(timerTask('middleware')); // ROUTES - if (hooks.beforeRoutes) { tasks.push(_toTask(hooks.beforeRoutes)); } + if (hooks.beforeRoutes) { + tasks.push(_toTask(hooks.beforeRoutes)); + } tasks.push(timerTask('beforeRoutes')); - tasks.push(function(done) { routes(done, app, options.routes_root_path); }); + tasks.push(function (done) { + routes(done, app, options.routes_root_path); + }); tasks.push(timerTask('routes')); - tasks.push(function(done) { wsRoutes(done, app, options.routes_root_path); }); + tasks.push(function (done) { + wsRoutes(done, app, options.routes_root_path); + }); tasks.push(timerTask('wsRoutes')); - tasks.push(function(done) { postRouteMiddleware(done, app, options.static_root_path); }); + tasks.push(function (done) { + postRouteMiddleware(done, app, options.static_root_path); + }); tasks.push(timerTask('postRouteMiddleware')); - if (hooks.afterRoutes) { tasks.push(_toTask(hooks.afterRoutes)); } + if (hooks.afterRoutes) { + tasks.push(_toTask(hooks.afterRoutes)); + } tasks.push(timerTask('afterRoutes')); - // START SERVER - tasks.push(function(done) { startServer(done, app, options); }); + tasks.push(function (done) { + startServer(done, app, options); + }); tasks.push(timerTask('startServer')); _statsd.increment('elephas.starting'); var _startTime = new Date(); - async.series(tasks, function(err, payload) { - if (err) { throw err; } + async.series(tasks, function (err, payload) { + if (err) { + throw err; + } var _startServerPayload = payload[payload.length - 2]; @@ -162,17 +179,16 @@ function elephas(options) { _statsd.timing('elephas.startup_time', _duration); - logger.debug('NODE_ENV: ' + process.env.NODE_ENV); - timerResults.forEach(function(t, i) { - var _start = i === 0 ? _startTime : timerResults[i-1].completed; + timerResults.forEach(function (t, i) { + var _start = i === 0 ? _startTime : timerResults[i - 1].completed; var _duration = t.completed - _start; logger.debug(_duration + 'ms', Array(8 - _duration.toString().length).join(' '), t.task); }); logger.debug('================================='); - logger.debug(_duration + 'ms', Array(8 - _duration.toString().length).join(' '), 'TOTAL STARTUP DURATION') + logger.debug(_duration + 'ms', Array(8 - _duration.toString().length).join(' '), 'TOTAL STARTUP DURATION'); }); } }; diff --git a/lib/logger.js b/lib/logger.js index 381702b..dd82740 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,3 +1,5 @@ +'use strict'; + var winston = require('winston'); var levels = { @@ -14,7 +16,7 @@ var levels = { failed: 1, success: 1, warning: 1 -} +}; var colors = { info: 'grey', @@ -25,17 +27,17 @@ var colors = { success: 'green', failed: 'red', query: 'magenta' -} +}; -var logger = new (winston.Logger)({ +var logger = new winston.Logger({ levels: levels, colors: colors }); logger.add(winston.transports.Console, { - timestamp: true, + timestamp: true, colorize: true, prettyPrint: true }); -module.exports = logger; +module.exports = logger; \ No newline at end of file diff --git a/lib/middleware.js b/lib/middleware.js index c80be09..e01b8f9 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -1,28 +1,23 @@ 'use strict'; -var bodyParser = require('body-parser'), - cookieParser = require('cookie-parser'), - session = require('express-session'), +var cookieParser = require('cookie-parser'), flash = require('express-flash'), - multer = require('multer'), helmet = require('helmet'), compression = require('compression'), expressLogger = require('./express-logger'); - -var RedisStore = require('connect-redis')(session); var timeout = require('connect-timeout'); var middlewareOptions = require('./middlewareOptions'); // var logger = require('./logger'); -module.exports = function(done, app, options) { +module.exports = function (done, app, options) { app.use(expressLogger.logger); app.use(expressLogger.errorLogger); if (options.csp !== false) { app.use(helmet.contentSecurityPolicy(middlewareOptions('csp', options))); } - if(options.frameguard !== false){ + if (options.frameguard !== false) { app.use(helmet.frameguard.apply(this, middlewareOptions('frameguard', options))); } app.use(helmet.xssFilter()); @@ -32,28 +27,35 @@ module.exports = function(done, app, options) { app.use(helmet.ieNoOpen()); app.use(cookieParser()); - if(options.session !== false ){ + if (options.session !== false) { + var session = require('express-session'); var _sessionOptions = middlewareOptions('session', options); _sessionOptions.store = _sessionOptions.store; - if (!_sessionOptions.store && options.redisClient) { - _sessionOptions.store = new RedisStore({client: options.redisClient, ttl: 60 * 60}); + var RedisStore = require('connect-redis')(session); + _sessionOptions.store = new RedisStore({ client: options.redisClient, ttl: 60 * 60 }); } app.use(session(_sessionOptions)); app.use(flash()); } var _bodyParserOptions = middlewareOptions('bodyParser', options); if (options.bodyParser !== false) { + var bodyParser = require('body-parser'); app.use(bodyParser.json(_bodyParserOptions.json)); app.use(bodyParser.urlencoded(_bodyParserOptions.urlencoded)); } - - app.use(multer(middlewareOptions('multer', options))); - app.use(compression(middlewareOptions('compression', options))); + app.use(flash()); + if (typeof options.multer !== 'undefined' && options.multer.disable !== true) { + var multer = require('multer'); + app.use(multer(middlewareOptions('multer', options))); + } + if (typeof options.compression !== 'undefined' && options.compression.disable !== true) { + app.use(compression(middlewareOptions('compression', options))); + } app.use(timeout(middlewareOptions('timeout', options).ms)); app.disable('x-powered-by'); done(); -}; +}; \ No newline at end of file diff --git a/lib/middlewareOptions.js b/lib/middlewareOptions.js index e3da095..40c1c93 100644 --- a/lib/middlewareOptions.js +++ b/lib/middlewareOptions.js @@ -1,3 +1,5 @@ +'use strict'; + var _ = require('lodash'); var logger = require('./logger'); @@ -19,9 +21,7 @@ var defaultOptions = { disableAndroid: true, // set to true if you want to disable Android (browsers can vary and be buggy) safari5: false // set to true if you want to force buggy CSP in Safari 5 }, - frameguard : [ - "SAMEORIGIN" - ], + frameguard: ["SAMEORIGIN"], hsts: { maxAge: 7776000000, includeSubdomains: true @@ -47,7 +47,7 @@ var defaultOptions = { }, multer: { dest: './tmp/uploads/', - rename: function (fieldname, filename) { + rename: function rename(fieldname, filename) { return fieldname + "_" + filename.replace(/\W+/g, '-').toLowerCase() + Date.now(); } }, diff --git a/lib/postRouteMiddleware.js b/lib/postRouteMiddleware.js index 673d045..6cc8350 100644 --- a/lib/postRouteMiddleware.js +++ b/lib/postRouteMiddleware.js @@ -2,9 +2,9 @@ var express = require('express'); -module.exports = function(done, app, static_root_path) { +module.exports = function (done, app, static_root_path) { if (static_root_path) { - app.use(express.static(static_root_path)); + app.use(express['static'](static_root_path)); } done(); diff --git a/lib/routes.js b/lib/routes.js index 84f5e0f..45560bf 100644 --- a/lib/routes.js +++ b/lib/routes.js @@ -1,3 +1,5 @@ +'use strict'; + var express = require('express'); var logger = require('./logger'); var findFiles = require('./findFiles'); @@ -18,49 +20,47 @@ function toRouter(r) { } function toLogs(files) { - var routes = _.chain(files) - .map(function(r) { - var routePaths = []; + var routes = _.chain(files).map(function (r) { + var routePaths = []; - _.forIn(r, function(v, k) { - routePaths.push(k); - }); + _.forIn(r, function (v, k) { + routePaths.push(k); + }); - return routePaths; - }) - .reduce(function(a, b) { return a.concat(b); }) - .sortBy() - .value(); + return routePaths; + }).reduce(function (a, b) { + return a.concat(b); + }).sortBy().value(); logger.debug('Added routes'); - routes.forEach(function(r) { + routes.forEach(function (r) { logger.debug(r); }); } -module.exports = function(cb, app, path) { +module.exports = function (cb, app, path) { var _paths = path; if (typeof path === 'string') { _paths = [path]; } - var tasks = _paths.map(function(p) { - return function(done) { - findFiles(p, '--routes.js', function(err, files) { + var tasks = _paths.map(function (p) { + return function (done) { + findFiles(p, '--routes.js', function (err, files) { if (err) { return done(err); } // Require route file and create express router - var routeFiles = files.map(function(f) { + var routeFiles = files.map(function (f) { var r = require(f); var urlPath = f.replace(p, '').split('/'); - var prefix = urlPath.filter(function(p, i) { - return i < (urlPath.length - 1); + var prefix = urlPath.filter(function (p, i) { + return i < urlPath.length - 1; }).join('/'); app.use('/', toRouter(r, prefix)); diff --git a/lib/services.js b/lib/services.js index 87e8eb9..1fefa8a 100644 --- a/lib/services.js +++ b/lib/services.js @@ -17,7 +17,7 @@ function initServices(err, files) { files = files || []; - services = files.map(function(f) { + services = files.map(function (f) { return { name: f.substr(f.lastIndexOf('/') + 1, f.length).replace('--service.js', ''), service: require(f), @@ -25,26 +25,26 @@ function initServices(err, files) { }; }); - services.forEach(function(s){ - s.service.on('connected', function(srv) { + services.forEach(function (s) { + s.service.on('connected', (function (srv) { var startupDuration = new Date() - s.startTime; logger.success(srv.name, 'service ready in ' + startupDuration + 'ms'); srv.connected = true; bootstrapServices(); - }.bind(this, s)); - - s.service.on('serviceDown', function(srv) { + }).bind(this, s)); + + s.service.on('serviceDown', (function (srv) { logger.error(srv.name, 'service is down. Cannot start app'); srv.connected = false; process.exit(); - }.bind(this, s)); + }).bind(this, s)); s.service.initialize(); }); } function execCallbacks() { - pending.forEach(function(callback) { + pending.forEach(function (callback) { callback(); }); } @@ -57,8 +57,7 @@ function bootstrapServices(cb, path) { if (!loaded_services) { loaded_services = true; findFiles(path, '--service.js', initServices); - } - else if (loaded_services && !services) { + } else if (loaded_services && !services) { // Execute all pending callbacks execCallbacks(); } @@ -66,7 +65,7 @@ function bootstrapServices(cb, path) { var connections; if (services) { - connections = services.filter(function(s) { + connections = services.filter(function (s) { return s.connected; }); } @@ -74,10 +73,9 @@ function bootstrapServices(cb, path) { if (connections && connections.length === services.length) { isReady = true; execCallbacks(); - } - else { + } else { if (cb) { - pending.push(cb); + pending.push(cb); } } } diff --git a/lib/startServer.js b/lib/startServer.js index a0f70da..858edf7 100644 --- a/lib/startServer.js +++ b/lib/startServer.js @@ -12,50 +12,47 @@ var logger = require('./logger'); var workersStarted = 0; -var startServer = function(done, app, options) { +var startServer = function startServer(done, app, options) { var server = options.server; var h; + if (typeof server.listen === 'undefined' || server.listen !== false) { + if (server.cluster) { + if (cluster.isMaster) { + // Fork workers. + for (var i = 0; i < numCPUs; i++) { + cluster.fork(); + } - if (server.cluster) { - if (cluster.isMaster) { - // Fork workers. - for (var i = 0; i < numCPUs; i++) { - cluster.fork(); - } + cluster.on('fork', function () { + workersStarted += 1; - cluster.on('fork', function() { - workersStarted += 1; + if (workersStarted === numCPUs) { + logger.success('Started at http://localhost:' + h.address().port + ' (using ' + numCPUs + ' cores)'); + } + }); - if (workersStarted === numCPUs) { - logger.success('Started at http://localhost:' + h.address().port + ' (using ' + numCPUs + ' cores)'); - } - }); + cluster.on('exit', function (worker, code, signal) { + logger.error('worker ' + worker.process.pid + ' died'); + }); + } else { + // Workers can share any TCP connection + // In this case its a HTTP server + h = http.createServer(app).listen(server.port); - cluster.on('exit', function(worker, code, signal) { - logger.error('worker ' + worker.process.pid + ' died'); + logger.success('Started worker pid: ' + cluster.worker.process.pid); + } + } else { + h = http.createServer(app); + h.listen(server.port, function () { + logger.success('Started at http://localhost:' + h.address().port + ' (single core)'); }); } - else { - // Workers can share any TCP connection - // In this case its a HTTP server - h = http.createServer(app) - .listen(server.port); - - logger.success('Started worker pid: ' + cluster.worker.process.pid); - } } - else { - h = http.createServer(app); - h.listen(server.port, function () { - logger.success('Started at http://localhost:' + h.address().port + ' (single core)'); - }); - } - // Websocket connection if (wsRouter.hasRoutes()) { var p = new Primus(h, { transformer: 'sockjs' }); - var _path = options.static; + var _path = options['static']; if (!_path.endsWith('/')) { _path += '/'; @@ -72,7 +69,7 @@ var startServer = function(done, app, options) { }); } - return done(null, {primus: p}); + return done(null, { primus: p }); }; -module.exports = startServer; +module.exports = startServer; \ No newline at end of file diff --git a/lib/statsd.js b/lib/statsd.js index 6146fef..8a0d710 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -1,19 +1,11 @@ 'use strict'; -var METHODS = [ - 'timing', - 'increment', - 'decrement', - 'histogram', - 'gauge', - 'set', - 'unique' -]; +var METHODS = ['timing', 'increment', 'decrement', 'histogram', 'gauge', 'set', 'unique']; var statsd_client = {}; -METHODS.forEach(function(k) { - statsd_client[k] = function(stat, value, sampleRate, tags, callback) { +METHODS.forEach(function (k) { + statsd_client[k] = function (stat, value, sampleRate, tags, callback) { // StatsD instance must be initialized with globalize=true if (global.statsd) { global.statsd[k](stat, value, sampleRate, tags, callback); diff --git a/lib/utils/SocketSubscriptionManager.js b/lib/utils/SocketSubscriptionManager.js index a2484ce..03595d0 100644 --- a/lib/utils/SocketSubscriptionManager.js +++ b/lib/utils/SocketSubscriptionManager.js @@ -1,9 +1,11 @@ +'use strict'; + var _ = require('lodash'); var _subscriptions = {}; var SocketSubscriptionManager = { - subscribe: function(client, key, stream$) { + subscribe: function subscribe(client, key, stream$) { if (!client || !key || !stream$) { console.log('elephas.SocketSubscriptionManager::Missing client or key or stream$', key, id, stream$); } @@ -16,32 +18,34 @@ var SocketSubscriptionManager = { _subscriptions[key][client.id] = stream$; // When the connection is closed - client.on('end', function() { - SocketSubscriptionManager.unsubscribeAll(client) + client.on('end', function () { + SocketSubscriptionManager.unsubscribeAll(client); }); }, - unsubscribe: function(client, key) { + unsubscribe: function unsubscribe(client, key) { var _stream$ = SocketSubscriptionManager.getStream$(client, key); if (_stream$) { _subscriptions[key][client.id] = null; - var _disposeable = _stream$.subscribe(function() {}); + var _disposeable = _stream$.subscribe(function () {}); return _disposeable.dispose(); } }, - unsubscribeAll: function(client) { - _.forIn(_subscriptions, function(v, key) { + unsubscribeAll: function unsubscribeAll(client) { + _.forIn(_subscriptions, function (v, key) { SocketSubscriptionManager.unsubscribe(client, key); }); }, - getStream$: function(client, key, create) { + getStream$: function getStream$(client, key, create) { if (_subscriptions[key] && _subscriptions[key][client.id]) { return _subscriptions[key][client.id]; } - if (!create) { return null; } + if (!create) { + return null; + } - const stream$ = create(); + var stream$ = create(); SocketSubscriptionManager.subscribe(client, key, stream$); @@ -49,4 +53,4 @@ var SocketSubscriptionManager = { } }; -module.exports = SocketSubscriptionManager; +module.exports = SocketSubscriptionManager; \ No newline at end of file diff --git a/lib/wsRouter.js b/lib/wsRouter.js index 93dde86..edeb9d9 100644 --- a/lib/wsRouter.js +++ b/lib/wsRouter.js @@ -1,3 +1,5 @@ +'use strict'; + var _ = require('lodash'); var logger = require('./logger'); var chalk = require('chalk'); @@ -7,11 +9,7 @@ var _routes; var actionStream$; function log(client, action) { - const msg = [ - chalk.gray('ACTION'), - action.type, - chalk.red('--unkown--' + client.id) - ].join(' '); + var msg = [chalk.gray('ACTION'), action.type, chalk.red('--unkown--' + client.id)].join(' '); logger.info(msg); } @@ -19,8 +17,8 @@ function log(client, action) { function init() { actionStream$ = new Rx.Subject(); - _.forIn(_routes, (action_fn, action_type) => { - var actionHandler$ = actionStream$.filter((payload) => { + _.forIn(_routes, function (action_fn, action_type) { + var actionHandler$ = actionStream$.filter(function (payload) { return payload.action.type === action_type; }); @@ -29,20 +27,20 @@ function init() { } var wsRouter = { - hasRoutes: function() { + hasRoutes: function hasRoutes() { return _routes ? true : false; }, - setActionHandlers: function(routes) { - _routes = _.reduce(routes, function(prev, next) { + setActionHandlers: function setActionHandlers(routes) { + _routes = _.reduce(routes, function (prev, next) { return _.merge(prev, next); }); init(); }, - route: function(client, action) { + route: function route(client, action) { log(client, action); - actionStream$.onNext({action, client}); + actionStream$.onNext({ action: action, client: client }); } }; -module.exports = wsRouter; +module.exports = wsRouter; \ No newline at end of file diff --git a/lib/wsRoutes.js b/lib/wsRoutes.js index 4a6a291..eb4c250 100644 --- a/lib/wsRoutes.js +++ b/lib/wsRoutes.js @@ -1,3 +1,5 @@ +'use strict'; + var logger = require('./logger'); var findFiles = require('./findFiles'); @@ -7,43 +9,41 @@ var _ = require('lodash'); var wsRouter = require('./wsRouter'); function toLogs(files) { - var routes = _.chain(files) - .map(function(r) { - var routePaths = []; + var routes = _.chain(files).map(function (r) { + var routePaths = []; - _.forIn(r, function(v, k) { - routePaths.push(k); - }); + _.forIn(r, function (v, k) { + routePaths.push(k); + }); - return routePaths; - }) - .reduce(function(a, b) { return a.concat(b); }) - .sortBy() - .value(); + return routePaths; + }).reduce(function (a, b) { + return a.concat(b); + }).sortBy().value(); logger.debug('Added routes'); - routes.forEach(function(r) { + routes.forEach(function (r) { logger.debug('ws://' + r); }); } -module.exports = function(cb, app, path) { +module.exports = function (cb, app, path) { var _paths = path; if (typeof path === 'string') { _paths = [path]; } - var tasks = _paths.map(function(p) { - return function(done) { - findFiles(p, '--ws.js', function(err, files) { + var tasks = _paths.map(function (p) { + return function (done) { + findFiles(p, '--ws.js', function (err, files) { if (err) { return done(err); } // Require route file and create express router - var routeFiles = files.map(function(f) { + var routeFiles = files.map(function (f) { var r = require(f); return r; }); diff --git a/package.json b/package.json index 87070de..b47d600 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "Some added sugar on top of express to give our our some sensible defaults and a little structure.", "main": "elephas.js", "scripts": { + "build": "rm -rf lib && NODE_ENV=production babel src --out-dir lib", "test": "grunt test", "test:watch": "npm run test -- --watch" }, diff --git a/src/Service.js b/src/Service.js new file mode 100644 index 0000000..a8d8df1 --- /dev/null +++ b/src/Service.js @@ -0,0 +1,38 @@ +'use strict'; + +var util = require("util"), + events = require("events"); + +function Service() { + events.EventEmitter.call(this); +} + + +util.inherits(Service, events.EventEmitter); + +// for connection retry. +Service.prototype.retryCount = 0; + +// max retry set as 3. +Service.prototype.MAX_RETRY = 3; + + +Service.prototype.connectionFailed = function() { + this.retryCount++; +}; + +Service.prototype.isRetry = function() { + return this.retryCount < this.MAX_RETRY; +}; + + +Service.prototype.serviceConnected = function() { + this.retryCount = 0; + this.emit('connected'); +}; + +Service.prototype.serviceDown = function() { + this.emit('serviceDown'); +}; + +module.exports = Service; \ No newline at end of file diff --git a/src/__test__/client-test.js b/src/__test__/client-test.js new file mode 100644 index 0000000..8622ae6 --- /dev/null +++ b/src/__test__/client-test.js @@ -0,0 +1,32 @@ +import {expect} from 'chai'; +import sinon from 'sinon'; +import client from '../client'; + +const payload = 'test'; + +global.Primus = function() { + return { + on(e, cb) { + return cb('payload'); + }, + write: sinon.spy() + }; +}; + +const myStore = { + dispatch: sinon.spy() +}; + +describe('client', () => { + it('subscribe to redux store', () => { + client.connect(myStore); + + expect(myStore.dispatch.called).to.equal(true); + }); + + it('should dispatch to server and client', () => { + client.dispatch('actionTest'); + + expect(myStore.dispatch.calledWith('actionTest')).to.equal(true); + }); +}); diff --git a/src/__test__/clientSocket-test.js b/src/__test__/clientSocket-test.js new file mode 100644 index 0000000..c180022 --- /dev/null +++ b/src/__test__/clientSocket-test.js @@ -0,0 +1,17 @@ +import {expect} from 'chai'; +import sinon from 'sinon'; +import clientSocket from '../clientSocket'; + +describe('clientSocket', () => { + it('should dispatch to the client', () => { + const spark = { + write: sinon.spy() + }; + + const client = clientSocket(spark); + + client.dispatch('myAction'); + + expect(spark.write.calledWith('myAction')).to.equal(true); + }); +}); diff --git a/src/__test__/wsRouter-test.js b/src/__test__/wsRouter-test.js new file mode 100644 index 0000000..03a7f81 --- /dev/null +++ b/src/__test__/wsRouter-test.js @@ -0,0 +1,46 @@ +import {expect} from 'chai'; +import sinon from 'sinon'; +import wsRouter from '../wsRouter'; + +const _testRoutes = [ + { MY_TEST_ACTION: sinon.spy() }, + { ANOTHER_ACTION: ()=>{ + var e = new Error('dummy'); + console.log(e.stack); + } + + } +]; + +describe('wsRouter', () => { + it('has no routes by default', () => { + expect(wsRouter.hasRoutes()).to.equal(false); + }); + + it('can add websocket routes/action handlers', () => { + wsRouter.setActionHandlers(_testRoutes); + + expect(wsRouter.hasRoutes()).to.equal(true); + }); + + it('can route to the correct action', () => { + const client = {}; + const action = {type: 'MY_TEST_ACTION', payload: 'test'}; + + wsRouter.route(client, action); + + const fn = _testRoutes[0].MY_TEST_ACTION; + + expect(fn.called).to.equal(true); + }); + + // it('should not execute an action handler if there is no match', () => { + // const client = {}; + // const action = {type: 'MISSING_ACTION', payload: 'test'}; + // _testRoutes[1].ANOTHER_ACTION = sinon.spy(); + // wsRouter.setActionHandlers([_testRoutes[1]]); + // wsRouter.route(client, action); + + // expect(_testRoutes[1].ANOTHER_ACTION.called).to.equal(false); + // }); +}); diff --git a/src/client.js b/src/client.js new file mode 100644 index 0000000..9a1f7f0 --- /dev/null +++ b/src/client.js @@ -0,0 +1,25 @@ +var _primus; +var _store; + +var elephas = { + connect: function(store) { + if (!_primus) { + _primus = new Primus(); + + _store = store; + + _primus.on('data', function(action) { + store.dispatch(action); + }); + } + }, + dispatch: function(action) { + _store.dispatch(action); + _primus.write(action); + }, + subscribe(cb) { + _primus.on('data', cb); + } +}; + +module.exports = elephas; diff --git a/src/clientSocket.js b/src/clientSocket.js new file mode 100644 index 0000000..33f811e --- /dev/null +++ b/src/clientSocket.js @@ -0,0 +1,27 @@ +var logger = require('./logger'); +var chalk = require('chalk'); + +function log(client, action) { + const msg = [ + chalk.gray('DISPATCH'), + action.type, + chalk.red('--unkown--' + client.id) + ].join(' '); + + logger.info(msg); +} + +var clientSocket = function clientSocket(spark) { + return { + id: spark.id, + on: function(e, cb) { + spark.on(e, cb); + }, + dispatch: function(action) { + spark.write(action); + log(this, action); + } + }; +}; + +module.exports = clientSocket; diff --git a/src/express-logger.js b/src/express-logger.js new file mode 100644 index 0000000..708deed --- /dev/null +++ b/src/express-logger.js @@ -0,0 +1,121 @@ +var logger = require('./logger'); +var chalk = require('chalk'); +var statsd = require('./statsd'); +var cluster = require('cluster'); + +function _calcLogLevel(statusCode) { + if (statusCode >= 500) { + return 'error'; + } + else if (statusCode >= 400) { + return 'warning'; + } + + return 'info'; +} + +function _formatStatus(statusCode) { + var color = 'gray'; + + if (statusCode >= 500) { + color = 'red'; + } + else if (statusCode >= 400) { + color = 'yellow'; + } + else if (statusCode >= 300) { + color = 'cyan'; + } + + return chalk[color](statusCode); +} + +function _formatResponseTime(ms) { + var color = 'gray'; + + if (ms >= 500) { + color = 'red'; + } + else if (ms >= 300) { + color = 'magenta'; + } + else if (ms >= 100) { + color = 'yellow'; + } + + return chalk[color](ms + 'ms'); +} + +function _heapDump() { + return {}; +} + +function _log(req, res, responseTime) { + var level = _calcLogLevel(res.statusCode); + + // var pid = cluster.isWorker ? cluster.worker.process.pid : process.pid; + + var user; + + if (req && req.user && req.user.username) { + user = chalk.gray(req.user.username); + } + else if (req && req.session && req.session.passport && req.session.passport.user) { + user = chalk.gray(req.session.passport.user); + } + else { + user = chalk.red('anonymous'); + } + + var msg = [ + chalk.gray(req.method), + _formatStatus(res.statusCode), + _formatResponseTime(responseTime), + req.originalUrl, + user, + // chalk.gray('pid=' + pid) + ].join(' '); + + logger[level](msg, _heapDump); + + statsd.timing('api.response_time', responseTime); +} + +process.on('uncaughtException', function(err) { + logger.error(err.stack); +}); + + +module.exports.logger = function(req, res, next) { + var _start = new Date(); + + // I feel really disgusted doing this. Can't waith to switch to Koa... + var end = res.end; + res.end = function(chunk, encoding) { + var responseTime = (new Date() - _start); + + res.end = end; + res.end(chunk, encoding); + + _log(req, res, responseTime); + }; + + next(); +}; + +module.exports.errorLogger = function(err, req, res, next) { + var _start = new Date(); + + // I feel really disgusted doing this. Can't waith to switch to Koa... + var end = res.end; + res.end = function(chunk, encoding) { + var responseTime = (new Date() - _start); + + res.end = end; + res.end(chunk, encoding); + + _log(req, res, responseTime); + }; + + next(); +}; diff --git a/src/findFiles.js b/src/findFiles.js new file mode 100644 index 0000000..4666c2c --- /dev/null +++ b/src/findFiles.js @@ -0,0 +1,16 @@ +'use strict'; + +var recursive = require('recursive-readdir'); + +module.exports = function findFiles(path, suffix, cb) { + recursive(path, function(err, files) { + if (err) { return cb ? cb(err) : null; } + + var routers = files.filter(function(f) { + var len = suffix.length; + return f.substr(f.length - len, len) === suffix; + }); + + return cb(null, routers); + }); +}; \ No newline at end of file diff --git a/src/framework.js b/src/framework.js new file mode 100644 index 0000000..b671f80 --- /dev/null +++ b/src/framework.js @@ -0,0 +1,182 @@ +'use strict'; + +var express = require('express'), + logger = require('./logger'), + async = require('async'), + _ = require('lodash'), + StatsD = require('node-statsd'); + +var _statsd = require('./statsd'); + + +var services = require('./services'), + middleware = require('./middleware'), + routes = require('./routes'), + wsRoutes = require('./wsRoutes'), + postRouteMiddleware = require('./postRouteMiddleware'), + startServer = require('./startServer'), + statsd_client = require('./statsd'); + +var DEFAULT_OPTIONS = { + httpsOnly: true, + ping: true, + server: { + port: 3000, + cluster: false, + listen: true + }, + logger: { + level: 'debug' + } +}; + +function _addStatsD(options, app) { + if (options) { + // TODO: Ideally we can avoid exposing this globally + options.globalize = true; + + new StatsD(options); + + app.use('/api', function(req, res, next) { + statsd_client.increment('api.request'); + next(); + }); + + logger.debug('Enabled statsd. prefix=' + options.prefix); + } +} + +function _HttpsOnly(options, app) { + if (options) { + app.use(function(req, res, next) { + var proto = req.headers['x-forwarded-proto'] || req.protocol; + + if (!proto || proto === 'https') { + next(); + } + else { + return res.redirect('https://' + req.headers.host + req.url); + } + }); + + logger.debug('Only https traffic is allowed'); + } +} + +function _ping(options, app) { + if (options) { + app.use('/ping', function(req, res) { + res.end('200 Service Available'); + }); + } +} + +function elephas(options) { + var app = express(); + options = _.defaults(options || {}, DEFAULT_OPTIONS); + + logger.transports.console.level = options.logger.level || 'debug'; + + + if (options.__dirname) { + options.routes_root_path = options.routes_root_path || options.__dirname; + options.services_root_path = options.services_root_path || options.__dirname; + options.static_root_path = options.static_root_path || options.__dirname + '/public'; + } + else { + logger.error('Require option `__dirname` in elephas config'); + process.exit(1); + } + + // _addKafkaLogger(options.logger); + _addStatsD(options.statsd, app); + + _ping(options.ping, app); + _HttpsOnly(options.httpsOnly, app); + + function _toTask(fn) { + return function(done) { + fn(done, app); + }; + } + + return { + app: app, + createServer: function(hooks) { + hooks = hooks || {}; + var timerResults = []; + + var timerTask = function(taskName) { + return function(done) { + timerResults.push({task: taskName, completed: new Date()}); + return done(); + }; + }; + + + var tasks = []; + + // SERVICES + if (hooks.beforeServices) { tasks.push(_toTask(hooks.beforeServices)); } + tasks.push(timerTask('beforeServices')); + tasks.push(function(done) { services(done, options.services_root_path); }); + tasks.push(timerTask('services')); + + // MIDDLEWARE + if (hooks.beforeMiddleware) { tasks.push(_toTask(hooks.beforeMiddleware)); } + tasks.push(timerTask('beforeMiddleware')); + tasks.push(function(done) { middleware(done, app, options, options.redisClient); }); + tasks.push(timerTask('middleware')); + + // ROUTES + if (hooks.beforeRoutes) { tasks.push(_toTask(hooks.beforeRoutes)); } + tasks.push(timerTask('beforeRoutes')); + tasks.push(function(done) { routes(done, app, options.routes_root_path); }); + tasks.push(timerTask('routes')); + + tasks.push(function(done) { wsRoutes(done, app, options.routes_root_path); }); + tasks.push(timerTask('wsRoutes')); + + tasks.push(function(done) { postRouteMiddleware(done, app, options.static_root_path); }); + tasks.push(timerTask('postRouteMiddleware')); + if (hooks.afterRoutes) { tasks.push(_toTask(hooks.afterRoutes)); } + tasks.push(timerTask('afterRoutes')); + + + // START SERVER + tasks.push(function(done) { startServer(done, app, options); }); + tasks.push(timerTask('startServer')); + + _statsd.increment('elephas.starting'); + var _startTime = new Date(); + + async.series(tasks, function(err, payload) { + if (err) { throw err; } + + var _startServerPayload = payload[payload.length - 2]; + + if (hooks.onComplete) { + hooks.onComplete(_startServerPayload); + } + + var _duration = new Date() - _startTime; + + _statsd.timing('elephas.startup_time', _duration); + + + logger.debug('NODE_ENV: ' + process.env.NODE_ENV); + + timerResults.forEach(function(t, i) { + var _start = i === 0 ? _startTime : timerResults[i-1].completed; + var _duration = t.completed - _start; + logger.debug(_duration + 'ms', Array(8 - _duration.toString().length).join(' '), t.task); + }); + + logger.debug('================================='); + logger.debug(_duration + 'ms', Array(8 - _duration.toString().length).join(' '), 'TOTAL STARTUP DURATION') + }); + } + }; +} + +module.exports = elephas; \ No newline at end of file diff --git a/src/logger.js b/src/logger.js new file mode 100644 index 0000000..381702b --- /dev/null +++ b/src/logger.js @@ -0,0 +1,41 @@ +var winston = require('winston'); + +var levels = { + // NPM style levels + error: 0, + warn: 1, + info: 2, + verbose: 3, + debug: 4, + silly: 5, + + // Custom + query: 1, + failed: 1, + success: 1, + warning: 1 +} + +var colors = { + info: 'grey', + debug: 'grey', + warn: 'yellow', + warning: 'yellow', + error: 'red', + success: 'green', + failed: 'red', + query: 'magenta' +} + +var logger = new (winston.Logger)({ + levels: levels, + colors: colors +}); + +logger.add(winston.transports.Console, { + timestamp: true, + colorize: true, + prettyPrint: true +}); + +module.exports = logger; diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 0000000..6c43a7e --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,61 @@ +'use strict'; + +var cookieParser = require('cookie-parser'), + flash = require('express-flash'), + helmet = require('helmet'), + compression = require('compression'), + expressLogger = require('./express-logger'); +var timeout = require('connect-timeout'); + +var middlewareOptions = require('./middlewareOptions'); +// var logger = require('./logger'); + +module.exports = function(done, app, options) { + app.use(expressLogger.logger); + app.use(expressLogger.errorLogger); + + if (options.csp !== false) { + app.use(helmet.contentSecurityPolicy(middlewareOptions('csp', options))); + } + if(options.frameguard !== false){ + app.use(helmet.frameguard.apply(this, middlewareOptions('frameguard', options))); + } + app.use(helmet.xssFilter()); + app.use(helmet.hsts(middlewareOptions('hsts', options))); + // app.use(helmet.crossdomain(middlewareOptions('crossdomain', options))); + app.use(helmet.noSniff()); + app.use(helmet.ieNoOpen()); + + app.use(cookieParser()); + if(options.session !== false ){ + var session = require('express-session'); + var _sessionOptions = middlewareOptions('session', options); + _sessionOptions.store = _sessionOptions.store; + if (!_sessionOptions.store && options.redisClient) { + var RedisStore = require('connect-redis')(session); + _sessionOptions.store = new RedisStore({client: options.redisClient, ttl: 60 * 60}); + } + app.use(session(_sessionOptions)); + app.use(flash()); + } + var _bodyParserOptions = middlewareOptions('bodyParser', options); + if (options.bodyParser !== false) { + var bodyParser = require('body-parser'); + app.use(bodyParser.json(_bodyParserOptions.json)); + app.use(bodyParser.urlencoded(_bodyParserOptions.urlencoded)); + } + + app.use(flash()); + if(typeof options.multer !== 'undefined' && options.multer.disable !== true){ + var multer = require('multer'); + app.use(multer(middlewareOptions('multer', options))); + } + if(typeof options.compression !== 'undefined' && options.compression.disable !== true){ + app.use(compression(middlewareOptions('compression', options))); + } + app.use(timeout(middlewareOptions('timeout', options).ms)); + + app.disable('x-powered-by'); + + done(); +}; diff --git a/src/middlewareOptions.js b/src/middlewareOptions.js new file mode 100644 index 0000000..e3da095 --- /dev/null +++ b/src/middlewareOptions.js @@ -0,0 +1,75 @@ +var _ = require('lodash'); +var logger = require('./logger'); + +var defaultOptions = { + csp: { + defaultSrc: ["'self'"], + scriptSrc: ["'self'"], + styleSrc: ["'self'"], + imgSrc: ["'self'"], + connectSrc: ["'self'"], + fontSrc: ["'self'"], + objectSrc: ["'self'"], + mediaSrc: ["'self'"], + frameSrc: ["'self'"], + // sandbox: ['allow-forms', 'allow-scripts'], + // reportUri: '/report-violation', + // reportOnly: false, // set to true if you only want to report errors + setAllHeaders: false, // set to true if you want to set all headers + disableAndroid: true, // set to true if you want to disable Android (browsers can vary and be buggy) + safari5: false // set to true if you want to force buggy CSP in Safari 5 + }, + frameguard : [ + "SAMEORIGIN" + ], + hsts: { + maxAge: 7776000000, + includeSubdomains: true + }, + // crossdomain: { + // caseSensitive: true + // }, + session: { + // cookie: {httpOnly: true, secure: true}, // This breaks the login!! + saveUninitialized: true, + resave: true, + + // Ideally, this should be provided + secret: 'F*_WgXEN6=V-7xJLKvKF%6-NnZR7j^_gJX*@B4cATw#6@X%wkHP*_zV_8_3zXQRuu!=kGyds&+TMp^KB&=h^@NPd_KXQ3q%EXd=eJZ8m$*Zr@@B2!9Q^nPypTtqX^upJ' + }, + bodyParser: { + json: { + limit: 10 * 1024 * 1024 //10MB limit + }, + urlencoded: { + extended: true + } + }, + multer: { + dest: './tmp/uploads/', + rename: function (fieldname, filename) { + return fieldname + "_" + filename.replace(/\W+/g, '-').toLowerCase() + Date.now(); + } + }, + compression: { + threshold: 1024 * 1 //1kb + }, + timeout: { + ms: 40 * 1000 //40 seconds + } +}; + +function validateOptions(key, options) { + if (key === 'session') { + if (!options.session === false && (!options.session || !options.session.secret)) { + logger.error('Should provide your own session secret in the elephas config.'); + } + } +} + +module.exports = function _middlewareOptions(key, options) { + validateOptions(key, options); + + var opt = _.defaults(options[key] || {}, defaultOptions[key]); + return opt; +}; \ No newline at end of file diff --git a/src/postRouteMiddleware.js b/src/postRouteMiddleware.js new file mode 100644 index 0000000..673d045 --- /dev/null +++ b/src/postRouteMiddleware.js @@ -0,0 +1,11 @@ +'use strict'; + +var express = require('express'); + +module.exports = function(done, app, static_root_path) { + if (static_root_path) { + app.use(express.static(static_root_path)); + } + + done(); +}; \ No newline at end of file diff --git a/src/routes.js b/src/routes.js new file mode 100644 index 0000000..84f5e0f --- /dev/null +++ b/src/routes.js @@ -0,0 +1,81 @@ +var express = require('express'); +var logger = require('./logger'); +var findFiles = require('./findFiles'); + +var async = require('async'); +var _ = require('lodash'); + +function toRouter(r) { + var router = express.Router(); + + for (var key in r) { + for (var method in r[key]) { + router[method.toLowerCase()](key, r[key][method]); + } + } + + return router; +} + +function toLogs(files) { + var routes = _.chain(files) + .map(function(r) { + var routePaths = []; + + _.forIn(r, function(v, k) { + routePaths.push(k); + }); + + return routePaths; + }) + .reduce(function(a, b) { return a.concat(b); }) + .sortBy() + .value(); + + logger.debug('Added routes'); + + routes.forEach(function(r) { + logger.debug(r); + }); +} + +module.exports = function(cb, app, path) { + var _paths = path; + + if (typeof path === 'string') { + _paths = [path]; + } + + var tasks = _paths.map(function(p) { + return function(done) { + findFiles(p, '--routes.js', function(err, files) { + if (err) { + return done(err); + } + + // Require route file and create express router + var routeFiles = files.map(function(f) { + var r = require(f); + + var urlPath = f.replace(p, '').split('/'); + + var prefix = urlPath.filter(function(p, i) { + return i < (urlPath.length - 1); + }).join('/'); + + app.use('/', toRouter(r, prefix)); + + // return {router: toRouter(r, prefix), path: '', name: prefix}; + return r; + }); + + // Log out all routes that were found and added + toLogs(routeFiles); + + return done(); + }); + }; + }); + + return async.series(tasks, cb); +}; \ No newline at end of file diff --git a/src/services.js b/src/services.js new file mode 100644 index 0000000..87e8eb9 --- /dev/null +++ b/src/services.js @@ -0,0 +1,85 @@ +'use strict'; + +var findFiles = require('./findFiles'), + logger = require('./logger'); + +var loaded_services = false; + +var isReady = false, + pending = [], + services = null; + +function initServices(err, files) { + if (err || files.length === 0) { + logger.warning('No services found'); + return bootstrapServices(); + } + + files = files || []; + + services = files.map(function(f) { + return { + name: f.substr(f.lastIndexOf('/') + 1, f.length).replace('--service.js', ''), + service: require(f), + startTime: new Date() + }; + }); + + services.forEach(function(s){ + s.service.on('connected', function(srv) { + var startupDuration = new Date() - s.startTime; + logger.success(srv.name, 'service ready in ' + startupDuration + 'ms'); + srv.connected = true; + bootstrapServices(); + }.bind(this, s)); + + s.service.on('serviceDown', function(srv) { + logger.error(srv.name, 'service is down. Cannot start app'); + srv.connected = false; + process.exit(); + }.bind(this, s)); + + s.service.initialize(); + }); +} + +function execCallbacks() { + pending.forEach(function(callback) { + callback(); + }); +} + +function bootstrapServices(cb, path) { + if (isReady && cb) { + return cb(); + } + + if (!loaded_services) { + loaded_services = true; + findFiles(path, '--service.js', initServices); + } + else if (loaded_services && !services) { + // Execute all pending callbacks + execCallbacks(); + } + + var connections; + + if (services) { + connections = services.filter(function(s) { + return s.connected; + }); + } + + if (connections && connections.length === services.length) { + isReady = true; + execCallbacks(); + } + else { + if (cb) { + pending.push(cb); + } + } +} + +module.exports = bootstrapServices; \ No newline at end of file diff --git a/src/startServer.js b/src/startServer.js new file mode 100644 index 0000000..e7bcefe --- /dev/null +++ b/src/startServer.js @@ -0,0 +1,78 @@ +'use strict'; + +var http = require('http'); +var cluster = require('cluster'); +var numCPUs = require('os').cpus().length; +var Primus = require('primus'); +var clientSocket = require('./clientSocket'); +var wsRouter = require('./wsRouter'); +var mkdirp = require('mkdirp'); + +var logger = require('./logger'); + +var workersStarted = 0; + +var startServer = function(done, app, options) { + var server = options.server; + var h; + if(typeof server.listen === 'undefined' || server.listen !== false){ + if (server.cluster) { + if (cluster.isMaster) { + // Fork workers. + for (var i = 0; i < numCPUs; i++) { + cluster.fork(); + } + + cluster.on('fork', function() { + workersStarted += 1; + + if (workersStarted === numCPUs) { + logger.success('Started at http://localhost:' + h.address().port + ' (using ' + numCPUs + ' cores)'); + } + }); + + cluster.on('exit', function(worker, code, signal) { + logger.error('worker ' + worker.process.pid + ' died'); + }); + } + else { + // Workers can share any TCP connection + // In this case its a HTTP server + h = http.createServer(app) + .listen(server.port); + + logger.success('Started worker pid: ' + cluster.worker.process.pid); + } + } + else { + h = http.createServer(app); + h.listen(server.port, function () { + logger.success('Started at http://localhost:' + h.address().port + ' (single core)'); + }); + } + } + // Websocket connection + if (wsRouter.hasRoutes()) { + var p = new Primus(h, { transformer: 'sockjs' }); + + var _path = options.static; + + if (!_path.endsWith('/')) { + _path += '/'; + } + + mkdirp.sync(_path + 'js'); + + p.save(_path + 'js/primus.js'); + + p.on('connection', function (spark) { + spark.on('data', function (action) { + wsRouter.route(clientSocket(spark), action); + }); + }); + } + + return done(null, {primus: p}); +}; + +module.exports = startServer; diff --git a/src/statsd.js b/src/statsd.js new file mode 100644 index 0000000..6146fef --- /dev/null +++ b/src/statsd.js @@ -0,0 +1,24 @@ +'use strict'; + +var METHODS = [ + 'timing', + 'increment', + 'decrement', + 'histogram', + 'gauge', + 'set', + 'unique' +]; + +var statsd_client = {}; + +METHODS.forEach(function(k) { + statsd_client[k] = function(stat, value, sampleRate, tags, callback) { + // StatsD instance must be initialized with globalize=true + if (global.statsd) { + global.statsd[k](stat, value, sampleRate, tags, callback); + } + }; +}); + +module.exports = statsd_client; \ No newline at end of file diff --git a/src/utils/SocketSubscriptionManager.js b/src/utils/SocketSubscriptionManager.js new file mode 100644 index 0000000..a2484ce --- /dev/null +++ b/src/utils/SocketSubscriptionManager.js @@ -0,0 +1,52 @@ +var _ = require('lodash'); + +var _subscriptions = {}; + +var SocketSubscriptionManager = { + subscribe: function(client, key, stream$) { + if (!client || !key || !stream$) { + console.log('elephas.SocketSubscriptionManager::Missing client or key or stream$', key, id, stream$); + } + + // Unsubscribe to any stream$ before adding new subscription + SocketSubscriptionManager.unsubscribe(client, key); + + // Add new subscription + _subscriptions[key] = _subscriptions[key] || {}; + _subscriptions[key][client.id] = stream$; + + // When the connection is closed + client.on('end', function() { + SocketSubscriptionManager.unsubscribeAll(client) + }); + }, + unsubscribe: function(client, key) { + var _stream$ = SocketSubscriptionManager.getStream$(client, key); + + if (_stream$) { + _subscriptions[key][client.id] = null; + var _disposeable = _stream$.subscribe(function() {}); + return _disposeable.dispose(); + } + }, + unsubscribeAll: function(client) { + _.forIn(_subscriptions, function(v, key) { + SocketSubscriptionManager.unsubscribe(client, key); + }); + }, + getStream$: function(client, key, create) { + if (_subscriptions[key] && _subscriptions[key][client.id]) { + return _subscriptions[key][client.id]; + } + + if (!create) { return null; } + + const stream$ = create(); + + SocketSubscriptionManager.subscribe(client, key, stream$); + + return stream$; + } +}; + +module.exports = SocketSubscriptionManager; diff --git a/src/wsRouter.js b/src/wsRouter.js new file mode 100644 index 0000000..93dde86 --- /dev/null +++ b/src/wsRouter.js @@ -0,0 +1,48 @@ +var _ = require('lodash'); +var logger = require('./logger'); +var chalk = require('chalk'); +var Rx = require('rx'); + +var _routes; +var actionStream$; + +function log(client, action) { + const msg = [ + chalk.gray('ACTION'), + action.type, + chalk.red('--unkown--' + client.id) + ].join(' '); + + logger.info(msg); +} + +function init() { + actionStream$ = new Rx.Subject(); + + _.forIn(_routes, (action_fn, action_type) => { + var actionHandler$ = actionStream$.filter((payload) => { + return payload.action.type === action_type; + }); + + action_fn(actionHandler$); + }); +} + +var wsRouter = { + hasRoutes: function() { + return _routes ? true : false; + }, + setActionHandlers: function(routes) { + _routes = _.reduce(routes, function(prev, next) { + return _.merge(prev, next); + }); + + init(); + }, + route: function(client, action) { + log(client, action); + actionStream$.onNext({action, client}); + } +}; + +module.exports = wsRouter; diff --git a/src/wsRoutes.js b/src/wsRoutes.js new file mode 100644 index 0000000..4a6a291 --- /dev/null +++ b/src/wsRoutes.js @@ -0,0 +1,62 @@ +var logger = require('./logger'); +var findFiles = require('./findFiles'); + +var async = require('async'); +var _ = require('lodash'); + +var wsRouter = require('./wsRouter'); + +function toLogs(files) { + var routes = _.chain(files) + .map(function(r) { + var routePaths = []; + + _.forIn(r, function(v, k) { + routePaths.push(k); + }); + + return routePaths; + }) + .reduce(function(a, b) { return a.concat(b); }) + .sortBy() + .value(); + + logger.debug('Added routes'); + + routes.forEach(function(r) { + logger.debug('ws://' + r); + }); +} + +module.exports = function(cb, app, path) { + var _paths = path; + + if (typeof path === 'string') { + _paths = [path]; + } + + var tasks = _paths.map(function(p) { + return function(done) { + findFiles(p, '--ws.js', function(err, files) { + if (err) { + return done(err); + } + + // Require route file and create express router + var routeFiles = files.map(function(f) { + var r = require(f); + return r; + }); + + wsRouter.setActionHandlers(routeFiles); + + // Log out all routes that were found and added + toLogs(routeFiles); + + return done(); + }); + }; + }); + + return async.series(tasks, cb); +}; \ No newline at end of file