mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-13 22:41:32 -05:00
8bc6a6e633
refs #827 - Moves ./index to use Ghost in a similar manner to how someone uses Ghost as an npm module. - Allows Ghost to be cleanly mounted on another express application on any arbitrary endpoint, all you need to customize is the mount path.
271 lines
9.9 KiB
JavaScript
271 lines
9.9 KiB
JavaScript
// # Custom Middleware
|
|
// The following custom middleware functions are all unit testable, and have accompanying unit tests in
|
|
// middleware_spec.js
|
|
|
|
var _ = require('lodash'),
|
|
express = require('express'),
|
|
busboy = require('./ghost-busboy'),
|
|
config = require('../config'),
|
|
path = require('path'),
|
|
api = require('../api'),
|
|
passport = require('passport'),
|
|
errors = require('../errors'),
|
|
utils = require('../utils'),
|
|
|
|
expressServer,
|
|
oauthServer,
|
|
loginSecurity = [],
|
|
forgottenSecurity = [];
|
|
|
|
function isBlackListedFileType(file) {
|
|
var blackListedFileTypes = ['.hbs', '.md', '.json'],
|
|
ext = path.extname(file);
|
|
return _.contains(blackListedFileTypes, ext);
|
|
}
|
|
|
|
function cacheServer(server) {
|
|
expressServer = server;
|
|
}
|
|
|
|
function cacheOauthServer(server) {
|
|
oauthServer = server;
|
|
}
|
|
|
|
var middleware = {
|
|
|
|
// ### Authenticate Middleware
|
|
// authentication has to be done for /ghost/* routes with
|
|
// exceptions for signin, signout, signup, forgotten, reset only
|
|
// api and frontend use different authentication mechanisms atm
|
|
authenticate: function (req, res, next) {
|
|
var path,
|
|
subPath;
|
|
|
|
// SubPath is the url path starting after any default subdirectories
|
|
// it is stripped of anything after the two levels `/ghost/.*?/` as the reset link has an argument
|
|
path = req.path;
|
|
/*jslint regexp:true, unparam:true*/
|
|
subPath = path.replace(/^(\/.*?\/.*?\/)(.*)?/, function (match, a) {
|
|
return a;
|
|
});
|
|
|
|
if (res.isAdmin) {
|
|
if (subPath.indexOf('/ghost/api/') === 0
|
|
&& path.indexOf('/ghost/api/v0.1/authentication/') !== 0) {
|
|
|
|
return passport.authenticate('bearer', { session: false, failWithError: true },
|
|
function (err, user, info) {
|
|
if (err) {
|
|
return next(err); // will generate a 500 error
|
|
}
|
|
// Generate a JSON response reflecting authentication status
|
|
if (! user) {
|
|
var msg = {
|
|
type: 'error',
|
|
message: 'Please Sign In',
|
|
status: 'passive'
|
|
};
|
|
res.status(401);
|
|
return res.send(msg);
|
|
}
|
|
// TODO: figure out, why user & authInfo is lost
|
|
req.authInfo = info;
|
|
req.user = user;
|
|
return next(null, user, info);
|
|
}
|
|
)(req, res, next);
|
|
}
|
|
}
|
|
next();
|
|
},
|
|
|
|
// ### CacheControl Middleware
|
|
// provide sensible cache control headers
|
|
cacheControl: function (options) {
|
|
/*jslint unparam:true*/
|
|
var profiles = {
|
|
'public': 'public, max-age=0',
|
|
'private': 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0'
|
|
},
|
|
output;
|
|
|
|
if (_.isString(options) && profiles.hasOwnProperty(options)) {
|
|
output = profiles[options];
|
|
}
|
|
|
|
return function cacheControlHeaders(req, res, next) {
|
|
if (output) {
|
|
res.set({'Cache-Control': output});
|
|
}
|
|
next();
|
|
};
|
|
},
|
|
|
|
// ### whenEnabled Middleware
|
|
// Selectively use middleware
|
|
// From https://github.com/senchalabs/connect/issues/676#issuecomment-9569658
|
|
whenEnabled: function (setting, fn) {
|
|
return function settingEnabled(req, res, next) {
|
|
// Set from server/middleware/index.js for now
|
|
if (expressServer.enabled(setting)) {
|
|
fn(req, res, next);
|
|
} else {
|
|
next();
|
|
}
|
|
};
|
|
},
|
|
|
|
staticTheme: function () {
|
|
return function blackListStatic(req, res, next) {
|
|
if (isBlackListedFileType(req.url)) {
|
|
return next();
|
|
}
|
|
|
|
return middleware.forwardToExpressStatic(req, res, next);
|
|
};
|
|
},
|
|
|
|
// to allow unit testing
|
|
forwardToExpressStatic: function (req, res, next) {
|
|
api.settings.read({context: {internal: true}, key: 'activeTheme'}).then(function (response) {
|
|
var activeTheme = response.settings[0];
|
|
|
|
express['static'](path.join(config.paths.themePath, activeTheme.value), {maxAge: utils.ONE_YEAR_MS})(req, res, next);
|
|
});
|
|
},
|
|
// ### Spam prevention Middleware
|
|
// limit signin requests to ten failed requests per IP per hour
|
|
spamSigninPrevention: function (req, res, next) {
|
|
var currentTime = process.hrtime()[0],
|
|
remoteAddress = req.connection.remoteAddress,
|
|
deniedRateLimit = '',
|
|
ipCount = '',
|
|
message = 'Too many attempts.',
|
|
rateSigninPeriod = config.rateSigninPeriod || 3600,
|
|
rateSigninAttempts = config.rateSigninAttempts || 10;
|
|
|
|
if (req.body.username && req.body.grant_type === 'password') {
|
|
loginSecurity.push({ip: remoteAddress, time: currentTime, email: req.body.username});
|
|
} else if (req.body.grant_type === 'refresh_token') {
|
|
return next();
|
|
} else {
|
|
return next(new errors.BadRequestError('No username.'));
|
|
}
|
|
|
|
// filter entries that are older than rateSigninPeriod
|
|
loginSecurity = _.filter(loginSecurity, function (logTime) {
|
|
return (logTime.time + rateSigninPeriod > currentTime);
|
|
});
|
|
|
|
// check number of tries per IP address
|
|
ipCount = _.chain(loginSecurity).countBy('ip').value();
|
|
deniedRateLimit = (ipCount[remoteAddress] > rateSigninAttempts);
|
|
|
|
if (deniedRateLimit) {
|
|
errors.logError(
|
|
'Only ' + rateSigninAttempts + ' tries per IP address every ' + rateSigninPeriod + ' seconds.',
|
|
'Too many login attempts.'
|
|
);
|
|
message += rateSigninPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
|
|
return next(new errors.UnauthorizedError(message));
|
|
}
|
|
next();
|
|
},
|
|
|
|
// ### Spam prevention Middleware
|
|
// limit forgotten password requests to five requests per IP per hour for different email addresses
|
|
// limit forgotten password requests to five requests per email address
|
|
spamForgottenPrevention: function (req, res, next) {
|
|
var currentTime = process.hrtime()[0],
|
|
remoteAddress = req.connection.remoteAddress,
|
|
rateForgottenPeriod = config.rateForgottenPeriod || 3600,
|
|
rateForgottenAttempts = config.rateForgottenAttempts || 5,
|
|
email = req.body.passwordreset[0].email,
|
|
ipCount = '',
|
|
deniedRateLimit = '',
|
|
deniedEmailRateLimit = '',
|
|
message = 'Too many attempts.',
|
|
index = _.findIndex(forgottenSecurity, function (logTime) {
|
|
return (logTime.ip === remoteAddress && logTime.email === email);
|
|
});
|
|
|
|
if (email) {
|
|
if (index !== -1) {
|
|
forgottenSecurity[index].count = forgottenSecurity[index].count + 1;
|
|
} else {
|
|
forgottenSecurity.push({ip: remoteAddress, time: currentTime, email: email, count: 0});
|
|
}
|
|
} else {
|
|
return next(new errors.BadRequestError('No email.'));
|
|
}
|
|
|
|
// filter entries that are older than rateForgottenPeriod
|
|
forgottenSecurity = _.filter(forgottenSecurity, function (logTime) {
|
|
return (logTime.time + rateForgottenPeriod > currentTime);
|
|
});
|
|
|
|
// check number of tries with different email addresses per IP
|
|
ipCount = _.chain(forgottenSecurity).countBy('ip').value();
|
|
deniedRateLimit = (ipCount[remoteAddress] > rateForgottenAttempts);
|
|
|
|
if (index !== -1) {
|
|
deniedEmailRateLimit = (forgottenSecurity[index].count > rateForgottenAttempts);
|
|
}
|
|
|
|
|
|
|
|
if (deniedEmailRateLimit) {
|
|
errors.logError(
|
|
'Only ' + rateForgottenAttempts + ' forgotten password attempts per email every ' +
|
|
rateForgottenPeriod + ' seconds.',
|
|
'Forgotten password reset attempt failed'
|
|
);
|
|
}
|
|
|
|
if (deniedRateLimit) {
|
|
errors.logError(
|
|
'Only ' + rateForgottenAttempts + ' tries per IP address every ' + rateForgottenPeriod + ' seconds.',
|
|
'Forgotten password reset attempt failed'
|
|
);
|
|
}
|
|
|
|
if (deniedEmailRateLimit || deniedRateLimit) {
|
|
message += rateForgottenPeriod === 3600 ? ' Please wait 1 hour.' : ' Please try again later';
|
|
return next(new errors.UnauthorizedError(message));
|
|
}
|
|
|
|
next();
|
|
},
|
|
resetSpamCounter: function (email) {
|
|
loginSecurity = _.filter(loginSecurity, function (logTime) {
|
|
return (logTime.email !== email);
|
|
});
|
|
},
|
|
|
|
// work around to handle missing client_secret
|
|
// oauth2orize needs it, but untrusted clients don't have it
|
|
addClientSecret: function (req, res, next) {
|
|
if (!req.body.client_secret) {
|
|
req.body.client_secret = 'not_available';
|
|
}
|
|
next();
|
|
},
|
|
|
|
// ### Authenticate Client Middleware
|
|
// authenticate client that is asking for an access token
|
|
authenticateClient: function (req, res, next) {
|
|
return passport.authenticate(['oauth2-client-password'], { session: false })(req, res, next);
|
|
},
|
|
|
|
// ### Generate access token Middleware
|
|
// register the oauth2orize middleware for password and refresh token grants
|
|
generateAccessToken: function (req, res, next) {
|
|
return oauthServer.token()(req, res, next);
|
|
},
|
|
|
|
busboy: busboy
|
|
};
|
|
|
|
module.exports = middleware;
|
|
module.exports.cacheServer = cacheServer;
|
|
module.exports.cacheOauthServer = cacheOauthServer;
|