diff --git a/package.json b/package.json index 4e96eda08e..1b20841b84 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "lodash": "^3.9.3", "loopback": "https://github.com/FreeCodeCamp/loopback.git#fix/no-password", "loopback-boot": "^2.8.0", - "loopback-component-passport": "1.4.0", + "loopback-component-passport": "^1.5.0", "loopback-connector-mongodb": "^1.10.0", "lusca": "~1.0.2", "method-override": "~2.3.0", diff --git a/server/middleware.json b/server/middleware.json index 664cacda58..10d78033a0 100644 --- a/server/middleware.json +++ b/server/middleware.json @@ -1,27 +1,60 @@ { "initial:before": { - "loopback#favicon": {} + "loopback#favicon": { + "params": "$!../public/favicon.ico" + } }, "initial": { - "compression": {} + "compression": {}, + "morgan": { + "params": "dev" + } }, "session": { + "./middlewares/sessions.js": {} + }, + "auth:before": { + "./middlewares/add-return-to": {} }, "auth": { }, "parse": { + "body-parser#json": {}, + "body-parser#urlencoded": { + "params": { "extended": true } + }, + "method-override": {}, + "./middlewares/cookie-parser": {} + }, + "parse:after": { + "./middlewares/validator": {} + }, + "routes:before": { + "express-flash": {}, + "helmet#xssFilter": {}, + "helmet#noSniff": {}, + "helmet#frameguard": {}, + "./middlewares/constant-headers": {}, + "./middlewares/csp": {}, + "./middlewares/express-rx": {}, + "./middlewares/global-locals": {} }, "routes": { }, "files": { "loopback#static": { - "params": "$!../public" + "params": [ + "$!../public", + { + "maxAge": "86400000" + } + ] } }, "final": { - "loopback#urlNotFound": {} }, "final:after": { - "errorhandler": {} + "./middlewares/keymetrics": {}, + "./middlewares/error-handlers": {} } } diff --git a/server/middlewares/add-return-to.js b/server/middlewares/add-return-to.js new file mode 100644 index 0000000000..9e9522b1ad --- /dev/null +++ b/server/middlewares/add-return-to.js @@ -0,0 +1,13 @@ +export default function addReturnToUrl() { + return function(req, res, next) { + // Remember original destination before login. + var path = req.path.split('/')[1]; + if (/auth|login|logout|signin|signup|fonts|favicon/i.test(path)) { + return next(); + } else if (/\/stories\/\w+/i.test(req.path)) { + return next(); + } + req.session.returnTo = req.path; + next(); + }; +} diff --git a/server/middlewares/constant-headers.js b/server/middlewares/constant-headers.js new file mode 100644 index 0000000000..28a1ad32bd --- /dev/null +++ b/server/middlewares/constant-headers.js @@ -0,0 +1,9 @@ +export default function constantHeaders() { + return function(req, res, next) { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', + 'Origin, X-Requested-With, Content-Type, Accept' + ); + next(); + }; +} diff --git a/server/middlewares/cookie-parser.js b/server/middlewares/cookie-parser.js new file mode 100644 index 0000000000..a991b5dc7e --- /dev/null +++ b/server/middlewares/cookie-parser.js @@ -0,0 +1,4 @@ +import cookieParser from 'cookie-parser'; +import secrets from '../../config/secrets'; + +export default cookieParser.bind(cookieParser, secrets.cookieSecret); diff --git a/server/middlewares/csp.js b/server/middlewares/csp.js new file mode 100644 index 0000000000..8354ce1df9 --- /dev/null +++ b/server/middlewares/csp.js @@ -0,0 +1,98 @@ +import helmet from 'helmet'; + +const trusted = [ + "'self'", + 'blob:', + '104.236.218.15', + '*.freecodecamp.com', + 'http://www.freecodecamp.com', + 'https://www.freecodecamp.com', + 'https://freecodecamp.com', + 'https://freecodecamp.org', + '*.freecodecamp.org', + // NOTE(berks): add the following as the blob above was not covering www + 'http://www.freecodecamp.org', + 'ws://freecodecamp.com/', + 'ws://www.freecodecamp.com/', + '*.gstatic.com', + '*.google-analytics.com', + '*.googleapis.com', + '*.google.com', + '*.gstatic.com', + '*.doubleclick.net', + '*.twitter.com', + '*.twitch.tv', + '*.twimg.com', + "'unsafe-eval'", + "'unsafe-inline'", + '*.bootstrapcdn.com', + '*.cloudflare.com', + 'https://*.cloudflare.com', + 'localhost:3001', + 'ws://localhost:3001/', + 'http://localhost:3001', + 'localhost:3000', + 'ws://localhost:3000/', + 'http://localhost:3000', + '*.ionicframework.com', + 'https://syndication.twitter.com', + '*.youtube.com', + '*.jsdelivr.net', + 'https://*.jsdelivr.net', + '*.ytimg.com', + '*.bitly.com', + 'http://cdn.inspectlet.com/', + 'https://cdn.inspeclet.com/', + 'wss://inspectletws.herokuapp.com/', + 'http://hn.inspectlet.com/', + '*.googleapis.com', + '*.gstatic.com', + 'https://hn.inspectlet.com/' +]; + +export default function csp() { + return helmet.csp({ + defaultSrc: trusted, + scriptSrc: [ + '*.optimizely.com', + '*.aspnetcdn.com', + '*.d3js.org', + 'https://cdn.inspectlet.com/inspectlet.js', + 'http://cdn.inspectlet.com/inspectlet.js', + 'http://beta.freecodecamp.com' + ].concat(trusted), + 'connect-src': [ + 'vimeo.com' + ].concat(trusted), + styleSrc: [ + '*.googleapis.com', + '*.gstatic.com' + ].concat(trusted), + imgSrc: [ + // allow all input since we have user submitted images for + // public profile + '*' + ].concat(trusted), + fontSrc: [ + '*.googleapis.com', + '*.gstatic.com' + ].concat(trusted), + mediaSrc: [ + '*.amazonaws.com', + '*.twitter.com' + ].concat(trusted), + frameSrc: [ + '*.gitter.im', + '*.gitter.im https:', + '*.vimeo.com', + '*.twitter.com', + '*.ghbtns.com' + ].concat(trusted), + // set to true if you only want to report errors + reportOnly: false, + // set to true if you want to set all headers + setAllHeaders: false, + // set to true if you want to force buggy CSP in Safari 5 + safari5: false + }); +} diff --git a/server/middlewares/error-handlers.js b/server/middlewares/error-handlers.js new file mode 100644 index 0000000000..c8eefaef08 --- /dev/null +++ b/server/middlewares/error-handlers.js @@ -0,0 +1,43 @@ +import errorHanlder from 'errorhandler'; +import accepts from 'accepts'; + +export default function prodErrorHandler() { + if (process.env.NODE_ENV === 'development') { + return errorHanlder({ log: true }); + } + // error handling in production. + // disabling eslint due to express parity rules for error handlers + return function(err, req, res, next) { // eslint-disable-line + // respect err.status + if (err.status) { + res.statusCode = err.status; + } + + // default status code to 500 + if (res.statusCode < 400) { + res.statusCode = 500; + } + + // parse res type + var accept = accepts(req); + var type = accept.type('html', 'json', 'text'); + + var message = 'opps! Something went wrong. Please try again later'; + if (type === 'html') { + req.flash('errors', { + msg: message + }); + return res.redirect('/'); + // json + } else if (type === 'json') { + res.setHeader('Content-Type', 'application/json'); + return res.send({ + message: message + }); + // plain text + } else { + res.setHeader('Content-Type', 'text/plain'); + return res.send(message); + } + }; +} diff --git a/server/middlewares/express-rx.js b/server/middlewares/express-rx.js new file mode 100644 index 0000000000..d687e38363 --- /dev/null +++ b/server/middlewares/express-rx.js @@ -0,0 +1,10 @@ +import { Observable } from 'rx'; + +// add rx methods to express +export default function expressRx() { + return function expressRx(req, res, next) { + // render to observable stream + res.render$ = Observable.fromNodeCallback(res.render, res); + next(); + }; +} diff --git a/server/middlewares/global-locals.js b/server/middlewares/global-locals.js new file mode 100644 index 0000000000..32a6148562 --- /dev/null +++ b/server/middlewares/global-locals.js @@ -0,0 +1,7 @@ +export default function globalLocals() { + return function(req, res, next) { + // Make user object available in templates. + res.locals.user = req.user; + next(); + }; +} diff --git a/server/middlewares/keymetrics.js b/server/middlewares/keymetrics.js new file mode 100644 index 0000000000..0902d6caab --- /dev/null +++ b/server/middlewares/keymetrics.js @@ -0,0 +1,8 @@ +import pmx from 'pmx'; + +export default function keymetrics() { + if (process.env.NODE_ENV !== 'production') { + return (err, req, res, next) => next(err); + } + return pmx.expressErrorHandler(); +} diff --git a/server/middlewares/sessions.js b/server/middlewares/sessions.js new file mode 100644 index 0000000000..fc0e4ccc1b --- /dev/null +++ b/server/middlewares/sessions.js @@ -0,0 +1,17 @@ +import session from 'express-session'; +import MongoStoreFactory from 'connect-mongo'; +import secrets from '../../config/secrets'; + +const MongoStore = MongoStoreFactory(session); + +export default function sessionsMiddleware() { + return session({ + resave: true, + saveUninitialized: true, + secret: secrets.sessionSecret, + store: new MongoStore({ + url: secrets.db, + 'autoReconnect': true + }) + }); +} diff --git a/server/middlewares/validator.js b/server/middlewares/validator.js new file mode 100644 index 0000000000..b40765bc89 --- /dev/null +++ b/server/middlewares/validator.js @@ -0,0 +1,9 @@ +import validator from 'express-validator'; + +export default validator.bind(validator, { + customValidators: { + matchRegex: function matchRegex(param, regex) { + return regex.test(param); + } + } +}); diff --git a/server/server.js b/server/server.js index fe0e924a55..3c047934e5 100755 --- a/server/server.js +++ b/server/server.js @@ -5,28 +5,9 @@ pmx.init(); var assign = require('lodash').assign, loopback = require('loopback'), boot = require('loopback-boot'), - accepts = require('accepts'), - cookieParser = require('cookie-parser'), - compress = require('compression'), - session = require('express-session'), expressState = require('express-state'), - logger = require('morgan'), - errorHandler = require('errorhandler'), - methodOverride = require('method-override'), - bodyParser = require('body-parser'), - helmet = require('helmet'), - MongoStore = require('connect-mongo')(session), - flash = require('express-flash'), path = require('path'), - expressValidator = require('express-validator'), - lessMiddleware = require('less-middleware'), - - passportProviders = require('./passport-providers'), - rxMiddleware = require('./utils/rx').rxMiddleware, - /** - * API keys and Passport configuration. - */ - secrets = require('./../config/secrets'); + passportProviders = require('./passport-providers'); var generateKey = require('loopback-component-passport/lib/models/utils').generateKey; @@ -45,176 +26,16 @@ var passportConfigurator = new PassportConfigurator(app); app.set('port', process.env.PORT || 3000); app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); - -app.use(compress()); -app.use(lessMiddleware(path.join(__dirname, '/public'))); -app.use(logger('dev')); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ - extended: true -})); -app.use(expressValidator({ - customValidators: { - matchRegex: function(param, regex) { - return regex.test(param); - } - } -})); -app.use(methodOverride()); -app.use(cookieParser(secrets.cookieSecret)); -app.use(session({ - resave: true, - saveUninitialized: true, - secret: secrets.sessionSecret, - store: new MongoStore({ - url: secrets.db, - 'autoReconnect': true - }) -})); - -app.use(flash()); app.disable('x-powered-by'); -app.use(helmet.xssFilter()); -app.use(helmet.noSniff()); -app.use(helmet.frameguard()); -app.use(function(req, res, next) { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Headers', - 'Origin, X-Requested-With, Content-Type, Accept' - ); - next(); -}); - -var trusted = [ - "'self'", - 'blob:', - '104.236.218.15', - '*.freecodecamp.com', - 'http://www.freecodecamp.com', - 'https://www.freecodecamp.com', - 'https://freecodecamp.com', - 'https://freecodecamp.org', - '*.freecodecamp.org', - // NOTE(berks): add the following as the blob above was not covering www - 'http://www.freecodecamp.org', - 'ws://freecodecamp.com/', - 'ws://www.freecodecamp.com/', - '*.gstatic.com', - '*.google-analytics.com', - '*.googleapis.com', - '*.google.com', - '*.gstatic.com', - '*.doubleclick.net', - '*.twitter.com', - '*.twitch.tv', - '*.twimg.com', - "'unsafe-eval'", - "'unsafe-inline'", - '*.bootstrapcdn.com', - '*.cloudflare.com', - 'https://*.cloudflare.com', - 'localhost:3001', - 'ws://localhost:3001/', - 'http://localhost:3001', - 'localhost:3000', - 'ws://localhost:3000/', - 'http://localhost:3000', - '*.ionicframework.com', - 'https://syndication.twitter.com', - '*.youtube.com', - '*.jsdelivr.net', - 'https://*.jsdelivr.net', - '*.ytimg.com', - '*.bitly.com', - 'http://cdn.inspectlet.com/', - 'https://cdn.inspeclet.com/', - 'wss://inspectletws.herokuapp.com/', - 'http://hn.inspectlet.com/', - '*.googleapis.com', - '*.gstatic.com', - 'https://hn.inspectlet.com/' -]; - -app.use(helmet.csp({ - defaultSrc: trusted, - scriptSrc: [ - '*.optimizely.com', - '*.aspnetcdn.com', - '*.d3js.org', - 'https://cdn.inspectlet.com/inspectlet.js', - 'http://cdn.inspectlet.com/inspectlet.js', - 'http://beta.freecodecamp.com' - ].concat(trusted), - 'connect-src': [ - 'vimeo.com' - ].concat(trusted), - styleSrc: [ - '*.googleapis.com', - '*.gstatic.com' - ].concat(trusted), - imgSrc: [ - /* allow all input since we have user submitted images for public profile*/ - '*' - ].concat(trusted), - fontSrc: [ - '*.googleapis.com', - '*.gstatic.com' - ].concat(trusted), - mediaSrc: [ - '*.amazonaws.com', - '*.twitter.com' - ].concat(trusted), - frameSrc: [ - '*.gitter.im', - '*.gitter.im https:', - '*.vimeo.com', - '*.twitter.com', - '*.ghbtns.com' - ].concat(trusted), - // set to true if you only want to report errors - reportOnly: false, - // set to true if you want to set all headers - setAllHeaders: false, - // set to true if you want to force buggy CSP in Safari 5 - safari5: false -})); - +// adds passport initialization after session middleware phase is complete passportConfigurator.init(); -// add rx methods to express -app.use(rxMiddleware()); - -app.use(function(req, res, next) { - - // Make user object available in templates. - res.locals.user = req.user; - next(); -}); - - -app.use( - loopback.static(path.join(__dirname, '../public'), { - maxAge: 86400000 - }) -); - boot(app, { appRootDir: __dirname, dev: process.env.NODE_ENV }); -app.use(function(req, res, next) { - // Remember original destination before login. - var path = req.path.split('/')[1]; - if (/auth|login|logout|signin|signup|fonts|favicon/i.test(path)) { - return next(); - } else if (/\/stories\/\w+/i.test(req.path)) { - return next(); - } - req.session.returnTo = req.path; - next(); -}); passportConfigurator.setupModels({ userModel: app.models.user, @@ -257,58 +78,6 @@ Object.keys(passportProviders).map(function(strategy) { ); }); -/** - * OAuth sign-in routes. - */ - -/** - * 500 Error Handler. - */ - -if (process.env.NODE_ENV === 'development') { - app.use(errorHandler({ - log: true - })); -} else { - app.use(pmx.expressErrorHandler()); - // error handling in production disabling eslint due to express parity rules - // for error handlers - app.use(function(err, req, res, next) { // eslint-disable-line - - // respect err.status - if (err.status) { - res.statusCode = err.status; - } - - // default status code to 500 - if (res.statusCode < 400) { - res.statusCode = 500; - } - - // parse res type - var accept = accepts(req); - var type = accept.type('html', 'json', 'text'); - - var message = 'opps! Something went wrong. Please try again later'; - if (type === 'html') { - req.flash('errors', { - msg: message - }); - return res.redirect('/'); - // json - } else if (type === 'json') { - res.setHeader('Content-Type', 'application/json'); - return res.send({ - message: message - }); - // plain text - } else { - res.setHeader('Content-Type', 'text/plain'); - return res.send(message); - } - }); -} - app.start = function() { app.listen(app.get('port'), function() { app.emit('started'); diff --git a/server/utils/rx.js b/server/utils/rx.js index ed7e0ebf73..7f0bac0704 100644 --- a/server/utils/rx.js +++ b/server/utils/rx.js @@ -30,12 +30,3 @@ exports.observableQueryFromModel = exports.observeMethod = function observeMethod(context, methodName) { return Rx.Observable.fromNodeCallback(context[methodName], context); }; - -// add rx methods to express -exports.rxMiddleware = function rxMiddleware() { - return function rxMiddleware(req, res, next) { - // render to observable - res.render$ = Rx.Observable.fromNodeCallback(res.render, res); - next(); - }; -};